Dmitry Nogin
Dmitry Nogin

Reputation: 3750

WPF Custom Control rerendering on view model subproperties change

Can WPF Custom Control trace view model subproperty changes to automatically rerender itself?

Let’s say that I have a model with two properties:

public class FullName : ViewModel
{
    string _first;
    string _last;

    public string First
    {
        get { return _first; }
        set
        {
            _first = value;
            RaisePropertyChanged();
        }
    }

    public string Last
    {
        get { return _last; }
        set
        {
            _last = value;
            RaisePropertyChanged();
        }
    }
}

Where ViewModel is:

public abstract class ViewModel : INotifyPropertyChanged
{
    protected void RaisePropertyChanged([CallerMemberName] string propertyName = null) =>
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) =>
        PropertyChanged?.Invoke(this, e);

    public event PropertyChangedEventHandler PropertyChanged;
}

I would like to have a Dependency Property on the WPF Custom Control (AffectsRender, no SubPropertiesDoNotAffectRender) to reference model in such a way that control automatically rerenders on First and Last property changes:

public class Tag : Control
{
    public static readonly DependencyProperty FullNameProperty =
        DependencyProperty.Register("FullName", typeof(FullName), typeof(Tag),
            new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));

    public FullName FullName
    {
        get { return (FullName)GetValue(FullNameProperty); }
        set { SetValue(FullNameProperty, value); }
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        if (FullName == null)
            return;

        FontFamily courier = new FontFamily("Courier New");
        Typeface courierTypeface = new Typeface(courier, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal);
        FormattedText ft2 = new FormattedText(FullName.First + " " + FullName.Last,
                                             CultureInfo.CurrentCulture,
                                             FlowDirection.LeftToRight,
                                             courierTypeface,
                                             14.0,
                                             Brushes.Black);

        drawingContext.DrawText(ft2, new Point());
    }
}

Here is the snippet to test it:

<Window x:Class="WpfApplication3.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApplication3"
    mc:Ignorable="d"
    Title="MainWindow" Height="139.9" Width="249.514">
  <StackPanel>
    <StackPanel.DataContext>
        <local:FullName>
            <local:FullName.First>John</local:FullName.First>
            <local:FullName.Last>Doe</local:FullName.Last>
        </local:FullName>
    </StackPanel.DataContext>
    <local:Tag FullName="{Binding}" Height="20"/>
    <TextBox Text="{Binding First, UpdateSourceTrigger=PropertyChanged}"/>
    <TextBox Text="{Binding Last, UpdateSourceTrigger=PropertyChanged}"/>
  </StackPanel>
</Window>

Unfortunately, it does not work – changes are not propagating to the custom control. Can it be done efficiently? What this SubPropertiesDoNotAffectRender is about then?

Upvotes: 3

Views: 667

Answers (1)

dymanoid
dymanoid

Reputation: 15227

For this to work, your FullName class has to be a Freezable and your First and Last properties have to be dependency properties.

You could take a look at the current implementation of the DependencyObject:

internal void NotifySubPropertyChange(DependencyProperty dp)
{
    InvalidateSubProperty(dp);

    // if the target is a Freezable, call FireChanged to kick off
    // notifications to the Freezable's parent chain.
    Freezable freezable = this as Freezable;
    if (freezable != null)
    {
        freezable.FireChanged();
    }
}

This mechanism was originally not intended for observing the sub-properties of a bound view-model. It helps to simplify the FrameworkElements measuring, arranging and rendering by observing the Freezables` properties and triggering appropriate actions on changing their properties and sub-properties.

You can find a nice blog post here that explains how does the retained graphics system in WPF work and how to use the feature you're interested in.

Upvotes: 1

Related Questions