Attached Properties don't work with Tab Control DataContextChanged event?

50 views Asked by At

Here's a problem with attached property when it comes to Tab Control.

Here's a user control:

public partial class ContentControlInstant : UserControl
{
    public ContentControlInstant()
    {
        InitializeComponent();

        Loaded += OnLoadedEvent;
        //DataContextChanged += OnDataContextEventChanged; //the attached property binding doesn't work if I set the binding at the DataContextChanged event?
    }

    private void OnDataContextEventChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue != null)
        {
            var printerHehe = "Printer BB";
            ExtraProperties.SetPrinter(this, printerHehe);
        }
    }

    private void OnLoadedEvent(object sender, RoutedEventArgs e)
    {
        var printerHehe = "Printer HAHAHA";
        ExtraProperties.SetPrinter(this, printerHehe);
    }


}

And this is how I define my ExtraProperties

 public static class ExtraProperties
   {

   public static readonly DependencyProperty PrinterProperty = DependencyProperty.RegisterAttached(
       "Printer", typeof(String), typeof(ExtraProperties), new PropertyMetadata(default(String)));

   public static String GetPrinter(DependencyObject obj)
   {
       return (String)obj.GetValue(PrinterProperty);
   }

   public static void SetPrinter(DependencyObject obj, String value)
   {

       obj.SetValue(PrinterProperty, value);
   }
}

Here's the ViewModel:

public class ContentControlVM: INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged();


        }
    }

    private string _printer;

    public string Printer
    {
        get => _printer;
        set
        {
            _printer = value;
            OnPropertyChanged();
        }
    }


    public string TabHeader { get; }
    public ContentControlVM(string tabHeader, string name)
    {
        TabHeader = tabHeader;
        Name = name;
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

public class MainWindowVM:INotifyPropertyChanged
{

    public TabbedExcelVM TabbedExcelVM { get;}
    public ContentControlVM ContentControlVM { get;}

    public MainWindowVM()
    {
        TabbedExcelVM =new TabbedExcelVM();
        ContentControlVM = new ContentControlVM("Separate", "Separate Name");


    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}




public class TabbedExcelVM:INotifyPropertyChanged
 {

     public IEnumerable<ContentControlVM> Tabs { get; }

     public TabbedExcelVM()
     {
         var tabs = new List<ContentControlVM>();
         tabs.Add(new ContentControlVM("AA", "Name AA"));
         tabs.Add(new ContentControlVM("BB", "Name BB"));
         Tabs = tabs;

     }

     public event PropertyChangedEventHandler PropertyChanged;

     protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
     {
         PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
     }

     protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
     {
         if (EqualityComparer<T>.Default.Equals(field, value)) return false;
         field = value;
         OnPropertyChanged(propertyName);
         return true;
     }
 }

And here's the MainWindow.xaml

<Window x:Class="AttachedPropertyBinding.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:AttachedPropertyBinding"
        xmlns:viewModel="clr-namespace:AttachedPropertyBinding.ViewModel"
        xmlns:ui="clr-namespace:AttachedPropertyBinding.UI"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <DataTemplate DataType="{x:Type viewModel:TabbedExcelVM}">
            <TabControl ItemsSource="{Binding Tabs}">
                <TabControl.ItemContainerStyle>
                    <Style TargetType="TabItem">
                        <Setter Property="Header" Value="{Binding TabHeader}"/>
                    </Style>
                </TabControl.ItemContainerStyle>
            </TabControl>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewModel:ContentControlVM}">
            <ui:ContentControlInstant 
                ui:ExtraProperties.Printer="{Binding Printer, Mode=OneWayToSource}">
            </ui:ContentControlInstant>
        </DataTemplate>
    </Window.Resources>

    <Window.DataContext>
        <viewModel:MainWindowVM/>
    </Window.DataContext>
    <ContentControl Content="{Binding TabbedExcelVM}"/>
    <!--<ContentControl Content="{Binding ContentControlVM}"/>-->
</Window>

What is important is that if I bind the ContentControl to a TabControl (TabbedExcelVM) as per the above,

The Printer is binded for the first TabItem, but not the second.

However, if I enable the line DataContextChanged += OnDataContextEventChanged; line, then amazingly, the binding will not happen at all! It seems that now, the attached Property Printer will not be binded to the underlying ViewModel Printer property.

Why is it happening, and how to fix this?

1

There are 1 answers

6
BionicCode On

There are happening two things:

  1. TabControl renders the TabItem content in a shared ContentPresenter. There is not an individual ContentPresenter for each TabItem. This makes sense as there is only a single tab visible at a time. Unless the data type doesn't change, the DataTemplate (used as value for the ContentPresenter.ContentTemplate) won't change too. Only the data bindings on the contained elements will update based on the changed data source. But, in your case, this means that because you have configured the Binding on the ExtraProperties.Printer attached property to operate as BindingMode.OneWayToSource, the Binding is only evaluated once - for the first data model. Remember, the first data model changes the presented data type of the ContentPresenter from null to ContentControlVM. This causes the DataTemplate to be loaded, which creates the reused instance of ContentControlInstant, which causes the Loaded event to be raised. Now, when switching between tabs, the same ContentControlInstant instance is used because the DataTemplate is reused and only the Binding objects are updated. And because you have configured the Binding to the Printer property as OneWayToSource nothing is going to happen - the target, ContentControlInstant instance, has not changed and therefore the Binding won't update the source (i.e. update source trigger is not satisfied).

    You can fix this by using the TabControl properly. Usually, you have similar tab headers but a completely different tab content. You can express this by modifying the data structure: the TabItem model must contain a property for the data model (composition) that can be assigned to the TabControl.Content property. If you don't do this, then the TabItem is implicitly assigned to the TabControl.Content property.

    The following simple example designs a data model for a two tab TabControl: FirstTabContentItem and SecondTabContentItem. Because each TabControl.Content value is now of a different type, a new DataTemplate is loaded. In your case a new instance of ContentControlInstant will be created each time, which would trigger the OneWayToSource Binding to update the source:

TabItemModel.cs

class TabItem : INotifyPropertyChanged
{
  // For complex headers create another data model used with a DataTemplate.
  // Otherwise, assign a simple string value.
  public object Header { get; set; }

  public ITabContentItem Content { get; set; }
}

ITabContentItem.cs

interface ITabContenItem
{
  string TextData { get; set; }
}

FirstTabContentItem.cs

class FirstTabContentItem : ITabContentItem
{}

SecondTabContentItem.cs

class SecondTabContentItem : ITabContentItem
{}

MainViewModel.cs

class MainViewModel : INotifyPropertyChanged
{
  public ObservableCollection<TabItem> TabItems { get; }

  public MainViewModel()
  {
    this.TabItems = new ObservableCollection<TabItem>
    {
      new TabItem() { Header = "Tab #1", Content = new FirstTabContentItem() { TextData = "Content #1" }},
      new TabItem() { Header = "Tab #2", Content = new SecondTabContentItem() { TextData = "Content #2" }},
    }
  }

MainWindow.xaml

<Window>
  <Window.DataContext>
    <MainViewModel />
  </Window.DataContext>

  <Window.Resources>
    <DataTemplate DataType="{x:Type FirstTabContentItem}">
      <TextBlock Text="{Binding TextData}" />
    </DataTemplate>

    <DataTemplate DataType="{x:Type SecondTabContentItem}">
      <TextBlock Text="{Binding TextData}" />
    </DataTemplate>

    <Style TargetType="TabItem">
      <Setter Property="Header"
              Value="{Binding Header}" />
      <Setter Property="Content"
              Value="{Binding Content}" />
    </Style>
  </Wndow.Resources>

  <TabControl ItemsSource={Binding TabItems}" />
</Window>
  1. The second effect you are experiencing is that you clear the Binding when setting the attached property. In WPF the dependency property system uses a priority system known as Dependency property precedence list. From the list you can see that local values have a very high precedence. Only animations and property coercion have a higher precedence. Assigning a local value always overwrites previous values (except the value comes from an animation or coerce callback), for example:
// Overwrites previous values including binding expressions
ExtraProperties.SetPrinter(this, printerHehe);

In your case the previous value was a binding expression. Because the Binding is now cleared, your XAML code does no longer behave as expected.

To fix it, define the dependency property to bind TwoWay by default (by configuring the FrameworkPropertyMetadata object) or explicitly configure the Binding to operate in TwoWay mode.
Then use the DependencyObject.SetCurrentValue method to set a value while bypassing the value precedence list. OneWay and OneWayToSource do not allow to preserve the previous value e.g. the Binding expression, as the dependency property system will use the binding source as value cache):

public static readonly DependencyProperty PrinterProperty = DependencyProperty.RegisterAttached(
  "Printer", 
  typeof(String), 
  typeof(ExtraProperties), 
  new FrameworkPropertyMetadata(
    default(String), 
    FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));

private void OnDataContextEventChanged(object sender, DependencyPropertyChangedEventArgs e)
{
  if (e.NewValue != null)
  {
    var printerHehe = "Printer BB";
    SetCurrentValue(ExtraProperties.PrinterProperty, printerHehe);
  }
}
<ui:ContentControlInstant ui:ExtraProperties.Printer="{Binding Printer}" />