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?
There are happening two things:
TabControlrenders theTabItemcontent in a sharedContentPresenter. There is not an individualContentPresenterfor eachTabItem. This makes sense as there is only a single tab visible at a time. Unless the data type doesn't change, theDataTemplate(used as value for theContentPresenter.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 theBindingon theExtraProperties.Printerattached property to operate asBindingMode.OneWayToSource, theBindingis only evaluated once - for the first data model. Remember, the first data model changes the presented data type of theContentPresenterfromnulltoContentControlVM. This causes theDataTemplateto be loaded, which creates the reused instance ofContentControlInstant, which causes the Loaded event to be raised. Now, when switching between tabs, the sameContentControlInstantinstance is used because theDataTemplateis reused and only theBindingobjects are updated. And because you have configured theBindingto thePrinterproperty asOneWayToSourcenothing is going to happen - the target,ContentControlInstantinstance, has not changed and therefore theBindingwon't update the source (i.e. update source trigger is not satisfied).You can fix this by using the
TabControlproperly. Usually, you have similar tab headers but a completely different tab content. You can express this by modifying the data structure: theTabItemmodel must contain a property for the data model (composition) that can be assigned to theTabControl.Contentproperty. If you don't do this, then theTabItemis implicitly assigned to theTabControl.Contentproperty.The following simple example designs a data model for a two tab
TabControl:FirstTabContentItemandSecondTabContentItem. Because eachTabControl.Contentvalue is now of a different type, a newDataTemplateis loaded. In your case a new instance ofContentControlInstantwill be created each time, which would trigger theOneWayToSourceBindingto update the source:TabItemModel.cs
ITabContentItem.cs
FirstTabContentItem.cs
SecondTabContentItem.cs
MainViewModel.cs
MainWindow.xaml
Bindingwhen 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:In your case the previous value was a binding expression. Because the
Bindingis now cleared, your XAML code does no longer behave as expected.To fix it, define the dependency property to bind
TwoWayby default (by configuring theFrameworkPropertyMetadataobject) or explicitly configure theBindingto operate inTwoWaymode.Then use the
DependencyObject.SetCurrentValuemethod to set a value while bypassing the value precedence list.OneWayandOneWayToSourcedo not allow to preserve the previous value e.g. theBindingexpression, as the dependency property system will use the binding source as value cache):