0

I've defined two UserControls :

  • Drawing: contains a CustomCanvas that derives from Canvas.
  • Control: contains a Button and is used to change the GlobalThickness property in MyViewModel.cs

The CustomCanvas has a custom dependency property named Thickness. This is bound to GlobalThickness in XAML.

I have also overridden the OnRender method in CustomCanvas to draw a Rectangle using a Pen its thickness is set to Thickness.

When I click the Button, the GlobalThickness changes and the Thickness which is bound to it changed as well. But I don't get a Rectangle with a new Thickness.

Here is all the code I've put together so far.

<Window x:Class="WpfApplication23.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        xmlns:local="clr-namespace:WpfApplication23">
    <Window.DataContext>
        <local:MyViewModel></local:MyViewModel>
    </Window.DataContext>
    <StackPanel>
        <local:Drawing/>
        <local:Control/>
    </StackPanel>
</Window>

<UserControl x:Class="WpfApplication23.Drawing"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:wpfApplication23="clr-namespace:WpfApplication23">
    <Grid>
        <wpfApplication23:CustomCanvas Thickness="{Binding GlobalThickness}"
                                       Height="100" 
                                       Width="100" 
                                       Background="Blue"/>
    </Grid>
</UserControl>

<UserControl x:Class="WpfApplication23.Control"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel>
        <Button Content="Change Thickness" 
                Width="200" 
                Height="30"
                Click="ButtonBase_OnClick"/>
    </StackPanel>
</UserControl>


public partial class Control
{
    public Control()
    {
        InitializeComponent();
    }

    private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
    {
        var vm = (MyViewModel)DataContext;
        vm.GlobalThickness = 10;
    }
}


public class CustomCanvas : Canvas
{
    public int Thickness
    {
        private get { return (int)GetValue(ThicknessProperty); }
        set
        {
            SetValue(ThicknessProperty, value); 
            InvalidateVisual();
        }
    }

    public static readonly DependencyProperty ThicknessProperty =
        DependencyProperty.Register("Thickness", typeof(int), typeof(CustomCanvas), new PropertyMetadata(0));

    protected override void OnRender(DrawingContext dc)
    {
        var myPen = new Pen(Brushes.Red, Thickness);
        var myRect = new Rect(0, 0, 400, 400);
        dc.DrawRectangle(null, myPen, myRect);
    }
}


public class MyViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private double _globalThickness = 1;
    public double GlobalThickness
    {
        get { return _globalThickness; }
        set
        {
            _globalThickness = value;
            RaisePropertyChanged("GlobalThickness");
        }
    }

    private void RaisePropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}
Vahid
  • 5,144
  • 13
  • 70
  • 146

2 Answers2

1

The property setter and hence the InvalidateVisual(); isn't called when the property is set by a binding. The reason is explained in XAML Loading and Dependency Properties.

You might instead register property metadata with your dependency property that forces re-rendering whenever the property value changes:

public static readonly DependencyProperty ThicknessProperty =
    DependencyProperty.Register(
        "Thickness", typeof(int), typeof(CustomCanvas),
        new FrameworkPropertyMetadata(
            default(int), FrameworkPropertyMetadataOptions.AffectsRender));

And is there any reason why Thickness is integer? It might as well be double:

public static readonly DependencyProperty ThicknessProperty =
    DependencyProperty.Register(
        "Thickness", typeof(double), typeof(CustomCanvas),
        new FrameworkPropertyMetadata(
            default(double), FrameworkPropertyMetadataOptions.AffectsRender));

public double Thickness
{
    get { return (double)GetValue(ThicknessProperty); }
    set { SetValue(ThicknessProperty, value); }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Thanks Clemens, but I'm getting `XamlParseException` in `Drawing.xaml` – Vahid Aug 13 '14 at 21:45
  • 1
    Did you try the `double` version? It needed to have `0d` or `0.0` as default value, instead of `0`. I've updated the answer. Even better, `default(double`)`. – Clemens Aug 13 '14 at 21:48
  • The updated answer works. Thank you so much. Am I on the right path? I'm trying to draw like hundreds of lines in the `OnRender` and then change their thickness on events. Of course I'll use drawing visual in the final solution. – Vahid Aug 13 '14 at 21:52
  • 1
    It should also be possible to change the Pen's Thickness after rendering. You would keep the Pen as member variable in your CustomCanvas, and change its Thickness in a PropertyChangedCallback (also registered with metadata). Then you wouldn't need the AffectsRender flag, and OnRender wouldn't be called frequently. – Clemens Aug 13 '14 at 21:56
  • This seems an awesome idea. Can you guide me in the right direction with some examples on SO or elsewhere? – Vahid Aug 13 '14 at 21:59
  • I don't know where to use `PropertyChangedCallback`? – Vahid Aug 13 '14 at 22:04
1

This alternative might be more efficient. Instead of frequently calling OnRender and re-rendering everything each time the Pen Thickness changed, it just changes the Pen's Thickness of an existing rendering which is made only once. The visual output will be updated automatically by WPF.

public class CustomCanvas : Canvas
{
    private readonly Pen pen = new Pen(Brushes.Red, 0d);

    public static readonly DependencyProperty ThicknessProperty =
        DependencyProperty.Register(
            "Thickness", typeof(double), typeof(CustomCanvas),
            new PropertyMetadata(ThicknessPropertyChanged));

    public double Thickness
    {
        get { return (double)GetValue(ThicknessProperty); }
        set { SetValue(ThicknessProperty, value); }
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        var myRect = ...
        drawingContext.DrawRectangle(null, pen, myRect);
    }

    private static void ThicknessPropertyChanged(
        DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        ((CustomCanvas)obj).pen.Thickness = (double)e.NewValue;
    }
}
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • Thank you so much Clemens, it might have a slight problem. It doesn't draw with no exception. – Vahid Aug 13 '14 at 22:12
  • Well, I've tested this and it works for me. Are you sure you're really using the member variable Pen in your OnRender override? – Clemens Aug 13 '14 at 22:16
  • Oh My mistake. This answer is very clever. So If I have like thousands of lines that are drawn with this pen. WPF won't re-render them when the thickness is changed? I'm a bit confused. After all new shapes with new thicknesses are drawn or am I missing something? – Vahid Aug 13 '14 at 22:20
  • 1
    Yes, finally something has to be re-drawn when the Pen changes. But this is low-level. The important difference is that your application code (your OnRender method) isn't called again. No new drawing has to be composed, the existing one is just drawn again with a modified Pen behind the scenes. So new thicknesses, yes, but no new shapes. – Clemens Aug 13 '14 at 22:23
  • I assume that this might be the most efficient method to draw lots of lines and change their thickness on events. What I'm trying to implement is drawing thousands of lines and then change their thickness on `MouseWheel` event to keep a zero-width line. I have implemented this using traditional way, clearing the canvas and redrawing. But the current method seems genius. I'll implement this method and let you know. – Vahid Aug 13 '14 at 22:28