WPF: Inheriting Property Values Considered
This article explains and compares some approaches to inherit property values down the Visual Tree.
Introduction
Our aim here is to set the same value on a specific property on all elements in the visual tree under some specific control. Like many things in WPF, there are a number of ways to approach this.
There are three samples which go with this article.
- Dependency Property Inheritance Experiments
- Set Child Properties by Walking the Visual Tree Sample
- Attached Dependency Property Inheritance Sample
Options
Some approaches are:
- Walk the Visual Tree using a recursive routine, and set the property.
- Use inheritance of an attached Dependency Property to get to all the children and set the property.
- Use Dependency Property inheritance to inherit the property value.
Dependency Property Inheritance
If you've read up about Dependency Properties you will know they offer a lot more functionality beyond regular properties. From MSDN:
A DependencyProperty supports the following capabilities in Windows Presentation Foundation (WPF):
The property can be set in a style. For more information, see Styling and Templating.
The property can be set through data binding. For more information about data binding dependency properties, see How to: Bind the Properties of Two Controls.
The property can be set with a dynamic resource reference. For more information, see XAML Resources.
The property can inherit its value automatically from a parent element in the element tree. For more information, see Property Value Inheritance.
The property can be animated. For more information, see Animation Overview.
The property can report when the previous value of the property has been changed and the property value can be coerced. For more information, see Dependency Property Callbacks and Validation.
The property reports information to WPF, such as whether changing a property value should require the layout system to recompose the visuals for an element.
The property receives support in the WPF Designer for Visual Studio. For example, the property can be edited in the Properties window.
On that long list of extras is inheriting values. Pretty clever stuff.
Example of Dependency Property Inheritance
You might think that IsEnabled is an example of such inheritance. After all, if you set IsEnabled=false on a panel such as a Grid then all it's children become disabled as well. IsEnabled is handled using CoerceValueCallBack rather than inheritance. Similar but different - this will be explained in a separate article which is to follow rather than complicate this one even more than it is already. This subject is pretty complicated as it is.
FontSize is a good example of Dependency Property Inheritance in that if you set FontSize on a window or parent container then the children inherit that value, unless you set a specific value on that property.
Consider the following markup:
<Window x:Class="wpf_Inheritance_Experiments.FontInheritance"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="FontInheritance" Height="300" Width="450"
FontSize="30">
<StackPanel>
<TextBlock Text="Size 30 inherited from Window"/>
<TextBlock FontSize="12"
Text="Size 12 set on TextBlock"/>
<Label FontSize="24">
<TextBlock Text="Size 24 Inherited from Label"/>
</Label>
</StackPanel>
</Window>
Which produces the below output:
As you can see, the FontSize is inherited from the Window, can be set on a control to over-ride it and can also be set on another container down the visual tree, inherited by controls beneath that.
This pretty simple stuff, but that window is part of the Inheritance Experiments sample, you will want to download that for later parts of the discussion anyway and it;s available from here.
That's it then, this article is just about how you set that inheritance?
Well it turns out that there's rather more to this Dependency Property inheritance than is immediately obvious and you might not always want to do your inheritance in that way.
As mentioned earlier, the devil is in the detail with this stuff. If you're used to reading the documentation you will know what to expect. Even so the critical part of the explanation on this is rather obscure.
The Cryptic Explanation:
If the page hadn't mysteriously disappeared from msdn you could read the following ( this is from google cache ).
Remarks
Property value inheritance is a feature of the WPF property system at the WPF framework level, whereby certain dependency properties can be locally set on an element at or near the root of a XAML element tree and then have their value inherited by all elements within the logical tree of child elements that also possess that property. Property value inheritance is not enabled by default, and enabling it does have some performance implications. For details, see Property Value Inheritance.
Note
Although property value inheritance might appear to work for nonattached dependency properties, the inheritance behavior for a nonattached property through certain object-object divisions in the runtime tree is undefined. Always use RegisterAttached to register properties where you specify Inherits in the metadata.
Properties on derived classes of PropertyMetadata are typically defined in the object model as read-write. This is so they can be adjusted after initialization of the instance. However, after the metadata is consumed as part of a call to Register, AddOwner, or OverrideMetadata, the property system will seal that metadata instance and properties that convey the specifics of the metadata are now considered immutable. Attempting to set this property after IsSealed is true on this metadata instance will raise an exception.
Explanation of the Explanation
What that means is when you want a Dependency Property to inherit down the visual tree to everything below whichever DependencyObject it is declared on, you need to do several things.
In brief:
- Mark with Inherits
- Use RegisterAttached rather than Register
As it turns out you have to be very careful in order to get this working reliably.
Parent Dependency Property
If you use the propdp snippet to generate a dependency property you get:
public int MyProperty
{
get { return (int)GetValue(MyPropertyProperty); }
set { SetValue(MyPropertyProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int), typeof(ownerclass), new PropertyMetadata(0));
You might reasonably expect to add something on that PropertyMetadata there but you will notice there is no such inherits property there.
You need to change that to FrameworkPropertyMetadata instead, like:
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.Register("MyProperty", typeof(int),
typeof(ownerclass),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.Inherits
));
That is still using DependencyProperty.Register though and the explanation in the msdn explanation emphasises that will not be reliable. You need to use RegisterAttached instead, thus:
public static readonly DependencyProperty MyPropertyProperty =
DependencyProperty.RegisterAttached("MyProperty", typeof(int),
typeof(ownerclass),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.Inherits
));
With RegisterAttached, a explained here the class which "owns" the dependency property must provide static accessor methods to get and set the value, the naming of these follows the pattern:
- public static object Get<PropertyName>
- public static void Set<PropertyName>
Where PropertName is the name you register the property - "MyProperty" in the above.
You will also want the usual property with get and set you'd expect on a dependency property.
There's example code later.
It is also advisable to make your property name unique - don't even think of doing this using a property name which is already in use in the framework and avoid duplicating your own property names.
Notice that the Dependency Property is associated specifically with that parent type.
Child Dependency Property
The dependency properties intended to inherit from that parent are defined using:
- A public static readonly dependency property ( with no metadata etc)
- A public property like you'd expect on a regular dependency property
- A static constructor on the control uses AddOwner to associate the dependency property with the parent
There is example code later which illlustrates this and you can see it working in one of the samples.
Implications
As well as the fact this is rather fiddly and hence error prone, there are some fairly major implications to this, in that:
- You need to know the type of the parent when you define your children
- You need to add code to your children
You can of course inherit and extend regular controls, but you need to do that for any controls you're going to use this approach with. Perhaps rather more practical is using this with ViewModels since you will of course be defining them yourself anyhow. That means you have to use Dependency Objects as ViewModels though with their disadvantages. The most significant being thread affinity, rather less significant being no serialisation.
The Experiments Sample
All that explanation is pretty convoluted and confusing with many things you must do. Therein lies the rub with this approach. It's very easy to get it wrong and tricky stuff to get working reliably.
The purpose of the sample is to explain just how easy it is to get something slightly wrong as well as what you do to get this working.
Until you see this code or try it yourself you probably won't appreciate just how tricky this is to work with. If you haven't already, you should download the sample from here so you can experiment with it for yourself.
The MainWindow acts as a sort of menu with a set of buttons in a ListBox which are used to show a Window per experiment.
How this works isn't really the focus of the article, but the name of each button is used to define which Window will be opened and all of them use the same click handler which is on the ListBox but handles all Button Click events from within that because it's a routed event which will "bubble up" the visual tree through the ListBox.
<ListBox HorizontalContentAlignment="Stretch"
Button.Click="Button_Click" >
<Button Name="No_DP_On_Child">No DP on Child</Button>
<Button Name="NotDirectParent">Not Direct Parent</Button>
<Button Name="DirectParent">Direct Parent</Button>
<Button Name="FontInheritance">Font Inheritance</Button>
</ListBox>
and
private void Button_Click(object sender, RoutedEventArgs e)
{
Button btn = e.OriginalSource as Button;
var win = (Window)System.Windows.Application.LoadComponent(new Uri(btn.Name + ".xaml", UriKind.Relative));
win.Show();
}
The ListBox just adds a little styling as you roll over the row a button is in, a StackPanel could have done the job.
WindowWithColour
Both the first two experiments inherit from a base window which has a SolidColorBrush dependency property defined.
public class WindowWithColour : Window
{
public static readonly DependencyProperty FillBrushProperty =
DependencyProperty.RegisterAttached("FillBrush",
typeof(SolidColorBrush),
typeof(WindowWithColour),
new FrameworkPropertyMetadata(
Brushes.LightGray,
FrameworkPropertyMetadataOptions.Inherits));
public static SolidColorBrush GetFillBrush(DependencyObject target)
{
return (SolidColorBrush)target.GetValue(FillBrushProperty);
}
public static void SetFillBrush(DependencyObject target, SolidColorBrush value)
{
target.SetValue(FillBrushProperty, value);
}
public SolidColorBrush FillBrush
{
get
{
return GetFillBrush(this);
}
set
{
SetFillBrush(this, value);
}
}
}
That uses RegisterAttached, the metadata has the Inherits flag set, there are the two static accessor methods mentioned above and they're used in the property get and set.
No DP On Child
The first experimental window looks like this running:
Immediately noticeable is that there is a purple ellipse in the top half there and nothing in the bottom.
Looking in the Output Window, there's this error:
System.Windows.Data Error: 40 : BindingExpression path error: 'FillBrush' property not found on 'object' ''Rectangle' (Name='')'. BindingExpression:Path=FillBrush; DataItem='Rectangle' (Name=''); target element is 'Rectangle' (Name=''); target property is 'Fill' (type 'Brush')
The markup for this is:
<local:WindowWithColour
x:Class="wpf_Inheritance_Experiments.No_DP_On_Child"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="No_DP_On_Child" Height="300" Width="300"
FillBrush="Purple"
xmlns:local="clr-namespace:wpf_Inheritance_Experiments"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<HeaderedContentControl
Grid.Row="0"
Header="Binding to Window property">
<Ellipse
Height="100" Width="100"
Fill="{Binding Path=FillBrush,
RelativeSource={RelativeSource AncestorType={x:Type local:WindowWithColour}}}"/>
</HeaderedContentControl>
<HeaderedContentControl
Grid.Row="1"
Header="Content Control without DP">
<Rectangle
Height="100" Width="200"
Fill="{Binding Path=FillBrush,
RelativeSource={RelativeSource Self}}"/>
</HeaderedContentControl>
</Grid>
</local:WindowWithColour>
The window inherits WindowWIthColour and hence has a FillBrush property which is set to Purple up there in the opening Window tag.
That purple circle has it's Fill bound directly to that FillBrush property on the Window.
That property is set with inherits but that error is telling you that the property isn't there on the Rectangle.
Well a Rectangle is a shape, it doesn't have any sort of a template maybe that's the problem there?
Change that binding to.
<Rectangle
Height="100" Width="200"
Fill="{Binding Path=FillBrush,
RelativeSource={RelativeSource AncestorType={x:Type HeaderedContentControl}}}"/>
Spin her up and see what happens.
Much the same result, just a slightly different error message.
System.Windows.Data Error: 40 : BindingExpression path error: 'FillBrush' property not found on 'object' ''HeaderedContentControl' (Name='')'. BindingExpression:Path=FillBrush; DataItem='HeaderedContentControl' (Name=''); target element is 'Rectangle' (Name=''); target property is 'Fill' (type 'Brush')
OK, that's pretty convincing. That property clearly isn't getting added to children down the visual tree.
NotDirectParent
Ok, so let's see if we can get this thing working. This version is similar but adds the necessary code to inherit that property from a child. There's a bit of a complication to doing this in that a Rectangle is a sealed class. That means you can't sub-class to extend it. The eagle eyed reader will have noticed that the two rows in the previous window each contained a HeaderedContentControl. This is there to both give the two sections a heading explaining what's going on and to so there's something there to subclass.
The markup is pretty similar:
<local:WindowWithColour x:Class="wpf_Inheritance_Experiments.NotDirectParent"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DP_On_Child" Height="300" Width="300"
FillBrush="Green"
xmlns:local="clr-namespace:wpf_Inheritance_Experiments"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<HeaderedContentControl
Grid.Row="0"
Header="Binding to Window property">
<Ellipse
Height="100" Width="100"
Fill="{Binding Path=FillBrush,
RelativeSource={RelativeSource AncestorType={x:Type local:WindowWithColour}}}"/>
</HeaderedContentControl>
<local:HeaderedContentControlWithColour
Grid.Row="1"
Header="Content Control with DP">
<Rectangle
Height="100" Width="200"
Fill="{Binding Path=FillBrush,
RelativeSource={RelativeSource AncestorType={x:Type local:HeaderedContentControlWithColour}}}"/>
</local:HeaderedContentControlWithColour>
</Grid>
</local:WindowWithColour>
FillBrush is set to Green in the Window tag. You can see the rectangle has it's Fill bound to FillBrush on HeaderedContentControlWithColour - which is that subclass of a HeaderedContentControl:
public class HeaderedContentControlWithColour : HeaderedContentControl
{
public static readonly DependencyProperty FillBrushProperty;
public SolidColorBrush FillBrush
{
get
{
return (SolidColorBrush)GetValue(FillBrushProperty);
}
set
{
SetValue(FillBrushProperty, value);
}
}
static HeaderedContentControlWithColour()
{
FillBrushProperty =
WindowWithColour.FillBrushProperty.AddOwner(typeof(HeaderedContentControlWithColour),
new FrameworkPropertyMetadata(
Brushes.LightGray,
FrameworkPropertyMetadataOptions.Inherits));
}
}
You can see here those rules on declaring a child dependency property which were explained earlier.
There's the static dependency property but with no metadata, the public property you'd expect for any dependency property. The particularly unusual thing here is the static constructor on that class which uses AddOwner to join that dependency property up to the one in WindowWithColour ( specifically ) and mark it's metadata with the Inherits flag.
Looking at the result in the designer, things don't look so good.
That's pretty strange, since clearly the property value is inherited down the visual tree.
Spin her up and you see, it works OK when you run it:
Well that's pretty weird. Something to put to the back of your mind should you use this yourself. You can't always trust the designer on this.
DirectParent
Let's take a look at a more direct implementation and see what happens with that.
<Window x:Class="wpf_Inheritance_Experiments.DirectParentxaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DirectParentxaml" Height="300" Width="300"
xmlns:local="clr-namespace:wpf_Inheritance_Experiments"
>
<local:GridWithColour FillBrush2="Orange">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<HeaderedContentControl
Grid.Row="0"
Header="Binding to Grid property">
<Ellipse
Height="100" Width="100"
Fill="{Binding Path=FillBrush2,
RelativeSource={RelativeSource AncestorType={x:Type local:GridWithColour}}}"/>
</HeaderedContentControl>
<local:HeaderedContentControlWithColour2
Grid.Row="1"
Header="Content Control with DP">
<Rectangle
Height="100" Width="200"
Fill="{Binding Path=FillBrush2,
RelativeSource={RelativeSource AncestorType={x:Type local:HeaderedContentControlWithColour2}}}"/>
</local:HeaderedContentControlWithColour2>
</local:GridWithColour>
</Window>
This window is just a regular one and GridWithColour is going to act as the parent control with the brush defined on it, inherited down into HeaderContentControlWithColour2.
public class GridWithColour : Grid
{
public static readonly DependencyProperty FillBrush2Property =
DependencyProperty.RegisterAttached(
"FillBrush2",
typeof(SolidColorBrush),
typeof(GridWithColour),
new FrameworkPropertyMetadata(Brushes.LightGray,
FrameworkPropertyMetadataOptions.Inherits));
public static SolidColorBrush GetFillBrush2(DependencyObject target)
{
return (SolidColorBrush)target.GetValue(FillBrush2Property);
}
public static void SetFillBrush2(DependencyObject target, SolidColorBrush value)
{
target.SetValue(FillBrush2Property, value);
}
public SolidColorBrush FillBrush2
{
get
{
return GetFillBrush2(this);
}
set
{
SetFillBrush2(this, value);
}
}
}
This is very similar to the base window approach, but inheriting from a Grid and the brush is called FillBrush2.
That HeaderedContentControl subclass is very similar to the one used earlier.
public class HeaderedContentControlWithColour2 : HeaderedContentControl
{
public static readonly DependencyProperty FillBrush2Property;
public SolidColorBrush FillBrush2
{
get
{
return (SolidColorBrush)GetValue(FillBrush2Property);
}
set
{
SetValue(FillBrush2Property, value);
}
}
static HeaderedContentControlWithColour2()
{
FillBrush2Property =
GridWithColour.FillBrush2Property.AddOwner(typeof(HeaderedContentControlWithColour2),
new FrameworkPropertyMetadata(
Brushes.LightGray,
FrameworkPropertyMetadataOptions.Inherits));
}
}
That is pointing to FillBrush2 on GridWithColour.
The designer looks good:
And when it's spun up, it works:
So that's what you might consider an ideal approach in that it works both in designer and when running.
Dependency Property Inheritance Conclusions
The article started out explaining that this is quite tricky and the experiments should have brought that home.
Here we're sub-classing and extending with new properties. Both the parent and child have quite specific code in them.
In many instances where you might want to consider using such inheritance, this is a nuisance. You need a specific class to be inheriting from and all controls which are going to inherit will need extending. As we've seen, that itself could be a problem since Rectangle is sealed and we needed to use a control as a container and bind to that.
This is the problem with using this on your own applications - where are you actually going to be able to do this.
There are some scenarios when this is a good way to go.
One of these is where you are using many lightweight dependency objects as viewmodels. This is most likely where you're presenting many graphics in one control and binding each. Particularly when performance is an issue since this is faster than alternatives.
These are relatively rare though.
So what alternatives are there?
We want some approach which is easier, not quite as fragile and suit those majority of use cases where inheriting DP isn't going to cut it.
Alternatives
The list of samples will already have given the game away here but let's see what we can do by writing some code. In order to illustrate these approaches with code we need a particular scenario.
Our requirement is to make everything within a Grid to read only, this was a forum question so it's a practical real world example.
Both these use very similar markup in order for you to compare the two approaches directly.
Walking the Visual Tree
This sample available here uses code to set a property on children down the Visual Tree from a parent Grid.
When you run the app, it looks like:
The markup for this is:
<Window x:Class="wpf_IsReadOnly_Grid.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:wpf_IsReadOnly_Grid"
>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="160"/>
</Grid.ColumnDefinitions>
<Border BorderBrush="Beige" BorderThickness="20"
Background="Beige">
<local:IsReadOnlyGrid IsReadOnly="{Binding IsChecked, ElementName=ToggleGrid}">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Text="Banana"/>
<TextBox Grid.Row="1" Text="Zebedee"/>
<ComboBox Name="Combo"
IsEditable="True"
Grid.Row="3"
>
<ComboBox.Items>
<ComboBoxItem>Arctic</ComboBoxItem>
<ComboBoxItem>Atlantic</ComboBoxItem>
<ComboBoxItem>Indian</ComboBoxItem>
<ComboBoxItem>Pacific</ComboBoxItem>
<ComboBoxItem>Southern</ComboBoxItem>
</ComboBox.Items>
</ComboBox>
<DataGrid ItemsSource="{Binding Chefs}" Grid.Row="5"/>
</local:IsReadOnlyGrid>
</Border>
<StackPanel Grid.Column="1">
<ToggleButton Name="ToggleGrid">Grid IsReadOnly</ToggleButton>
<ToggleButton IsChecked="{Binding IsReadOnly, Mode=TwoWay, ElementName=Combo}">
Combo IsReadOnly
</ToggleButton>
</StackPanel>
</Grid>
</Window>
There's an IsReadOnlyGrid, inside of which are a couple of TextBox, a ComboBox and a DataGrid.
There's an IsReadOnly property on that Grid which is bound to a ToggleButton and the IsReadOnly on the ComboBox is bound to another ToggleButton.
If you try these out you will find the "Grid IsReadOnly" button toggles IsReadOnly on all the contents of the Grid, the "Combo IsReadOnly" toggle will over-ride that setting on the ComboBox.
Note that it's the TextBox part of the ComboBox which this applies to - that can be a bit confusing if you're not familiar with editable comboboxes.
The DataGrid gets it's data via MainWindowViewModel which exposes a public ObservableCollection<Person> which is initialised with the names of some celebrity chefs. Since this is not really the focus of the article let's gloss over the details of that though.
IsReadOnlyGrid
IsReadOnlyGrid inherits from Grid in order to add an IsReadOnly dependency property, and some logic. That allows you to set and bind IsReadOnly on IsReadOnlyGrid like you would expect to with any property on a regular control.
public class IsReadOnlyGrid : Grid
{
public bool IsReadOnly
{
get { return (bool)GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(IsReadOnlyGrid),
new PropertyMetadata(false,
new PropertyChangedCallback(OnIsReadOnlyChanged)
));
private static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
SetChildren(d, (bool)e.NewValue);
}
private static void SetChildren(DependencyObject parent, bool isReadonly)
{
var children = LogicalTreeHelper.GetChildren(parent);
foreach (object obj in LogicalTreeHelper.GetChildren(parent))
{
if (obj is DependencyObject)
{
DependencyObject dob = obj as DependencyObject;
var flags = BindingFlags.Static |
BindingFlags.FlattenHierarchy |
BindingFlags.Public;
var fi = obj.GetType().GetField("IsReadOnlyProperty", flags);
if (fi != null)
{
DependencyProperty dp = fi.GetValue(null) as DependencyProperty;
dob.SetCurrentValue(dp, isReadonly);
}
SetChildren(dob, isReadonly);
}
}
}
public IsReadOnlyGrid()
{
this.Loaded += IsReadOnlyGrid_Loaded;
}
void IsReadOnlyGrid_Loaded(object sender, RoutedEventArgs e)
{
SetChildren(this, IsReadOnly);
}
}
You can see IsReadOnly is a regular dependency property. The dependency property callback will fire when the property value changes and is used to drive walking the visual tree. Since this is being called from a Static, the method is also Static itself and uses objects passed in via the call rather than any in the parent object. This makes coding mildly inconvenient.
LogicalTreeHelper is used to get the children using a pretty standard approach.
A complication is that not all controls have an IsReadOnly property - In fact most will not. That piece of LINQ there is getting any IsReadOnlyProperty Field. Field is perhaps a little counter-intuitive since you might think this is a property, but dependency properties aren't regular properties.
SetCurrentValue is used in order to avoid over-writing any bindings and so any specific value set will over-ride the "inherited" one, should that be necessary.
Plusses
We have precise control over what's going on here and this approach will be robust.
Minusses
The code necessary to walk the visual tree is a bit cumbersome and this will not be as efficient as an inherited dependency property.
Maybe we could do something about that.
Attached Dependency Property Inheritance
The final sample which you can download from here uses a mix of setting properties in code and inherits.
As mentioned earlier, this is very similar to the previous sample, the different background colour is to make it obvious which one you're looking at.
The markup for this is also pretty similar:
<Window x:Class="wpf_ReadOnly_Attached.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:wpf_ReadOnly_Attached"
>
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="160"/>
</Grid.ColumnDefinitions>
<Border BorderBrush="AliceBlue" BorderThickness="20"
Background="AliceBlue">
<Grid local:AttachDO.ReadOnly="{Binding IsChecked, ElementName=ToggleGrid}">
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="30"/>
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBox Text="Apple"/>
<TextBox Grid.Row="1" Text="Balloon"/>
<ComboBox Name="Combo"
IsEditable="True"
Grid.Row="3"
>
<ComboBox.Items>
<ComboBoxItem>Arctic</ComboBoxItem>
<ComboBoxItem>Atlantic</ComboBoxItem>
<ComboBoxItem>Indian</ComboBoxItem>
<ComboBoxItem>Pacific</ComboBoxItem>
<ComboBoxItem>Southern</ComboBoxItem>
</ComboBox.Items>
</ComboBox>
<DataGrid ItemsSource="{Binding Chefs}" Grid.Row="5"/>
</Grid>
</Border>
<StackPanel Grid.Column="1">
<ToggleButton Name="ToggleGrid">Grid IsReadOnly</ToggleButton>
<ToggleButton IsChecked="{Binding IsReadOnly, Mode=TwoWay, ElementName=Combo}">
Combo IsReadOnly
</ToggleButton>
</StackPanel>
</Grid>
</Window>
The particular thing to notice is that there is the line
<Grid local:AttachDO.ReadOnly="{Binding IsChecked, ElementName=ToggleGrid}">
Where you can see there is an Attached Dependency Object called ReadOnly which is applied to that Grid. Notice also that in this case the second ToggleButton is bound to IsReadOnly on the ComboBox.
Let's take a look at that Attached Dependency Property.
AttachDO
This Dependency Property can theoretically be attached to any DependencyObject, but of course you need a container that has some UIElements in it which have the IsReadOnly property on them before it'd do anything meaningful.
public class AttachDO : DependencyObject
{
public static bool GetReadOnly(DependencyObject d)
{
return (bool)d.GetValue(ReadOnlyProperty);
}
public static void SetReadOnly(DependencyObject d, bool value)
{
d.SetValue(ReadOnlyProperty, value);
}
public static readonly DependencyProperty ReadOnlyProperty = DependencyProperty.RegisterAttached("ReadOnly",
typeof(bool), typeof(AttachDO),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.Inherits,
OnReadOnlyChanged)
);
private static void OnReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var ui = d as UIElement;
if (ui != null)
{
var flags = BindingFlags.Static |
BindingFlags.FlattenHierarchy |
BindingFlags.Public;
var fi = ui.GetType().GetField("IsReadOnlyProperty", flags);
if (fi != null)
{
DependencyProperty dp = fi.GetValue(null) as DependencyProperty;
if (!dp.ReadOnly)
{
ui.SetCurrentValue(dp, (bool)e.NewValue);
}
}
}
}
}
This is a largely standard Attached Dependency Property other than the fact it has metadata marking it as Inherits and there's a Callback which will run on all DependencyObjects within the virtual tree under the one it is attached to.
Notice that the children have no Dependency Property defined inheriting this. Remember that experiment up there with the purple ellipse? Attached Dependency Properties are rather more successful at propagating down the visual tree than Dependency Properties.
In a similar way to the previous sample, only UIElements can have IsReadOnly so everything else is ignored. LINQ is used to find if there is a IsReadOnly property on each of these. There's an odd edge case which will cause an error if you try and set a dependency property on a dependency property marked as ReadOnly so any of these are ignored. SetCurrentValue is then used to avoid over-writing any bindings.
Plusses
Walking the tree is obviated.
Minusses
It seems strange that attached properties work differently to regular dependency properties and one could imagine some future rationalisation of some sort.
In the meantime this approach is quite convenient.
Summary
You've seen several approaches and their various aspects were explored and discussed. You should now be in a better position to decide which technique would best suit your projects and the potential pitfalls which are to be avoided.