2

I have an attached property that specifies the "inherits" option to achieve WPF property value inheritance. I can see that the property value is propagated across the visual tree. However, with large visual trees, this may impact performance quite a bit.

I would therefore like the attached property value inheritance of my attached property to stop at certain boundaries, more specifically instances of a particular class.

I have read about FrameworkElement.InheritanceBehavior, which a control can set to something like SkipAllNext, which stops property value inheritance (for all properties, though), but also affects resource lookup. The effect on resource lookup is not desirable.

Is there any other way to control the propagation, either in the attached property or in the class that should act as a boundary?

What I am trying to achieve is here: WPF container to turn all child controls to read-only. The solution with value inheritance to have all controls in a form turn to read-only based on a global switch is pretty good. It just has the performance penalty as mentioned there and here.

Community
  • 1
  • 1
user1211286
  • 681
  • 5
  • 17
  • 1
    First up - don't worry about it until its a problem! Second, you could rebind the property of a control to a `FindAncestor` `RelativeSource` in portions of your visual tree that are known. What are you achieving with the inheritence. Perhaps there is another mechanism entirely! – Gusdor Dec 04 '13 at 13:21
  • 1
    Actually, it *is* a problem. I should have written "...does impact performance". The issues is basically with DataGrid having many cells, which easily causes the propagation to go into hundreds to thousands of elements - where the property is not actually needed. – user1211286 Dec 04 '13 at 15:02
  • Is the property needed below the cells? Set the property to `{x:Null} ` on their parent. This will mean that the change only propagates once. – Gusdor Dec 04 '13 at 15:26
  • @user1211286 Does changing the DataContext have the same performance impact? If the issue is propagation, it should... I would guess the actual problem is either your PropertyChangedCallback is inefficient or setting IsReadOnly on your DataGrid is simply too slow on its own. – nmclean Dec 05 '13 at 13:54
  • The issue is not property changes, the performance isse is even with the initial setter. If the property is propagated through the entire visual tree including all grid cells, then we are looking at a very large number of UI elements for which the setter is called. Even if each call is quick, the total is substantial. Since I am already dealing with the grid itself, there is no need to even consider all its cells. – user1211286 Dec 05 '13 at 15:24
  • @user1211286 The PropertyChangedCallback is called for the initial set as well. By default, the DataGrid's IsReadOnly is false, and then is flipped to true when it inherits the attached property. Try to remove the attached property and just switch IsReadOnly to true on DataGrid.Load. It still needs to re-evaluate all the cells, and I suspect that's where the main bottleneck is. – nmclean Dec 05 '13 at 16:59

3 Answers3

1

AddOwner seems to work:

class BoundaryElement : FrameworkElement {
    public static readonly DependencyProperty CustomProperty =
        AttachedProperties.CustomProperty.AddOwner(typeof(BoundaryElement),
            new FrameworkPropertyMetadata() {Inherits = false});
}

I tried setting Inherits = false through OverrideMetadata, but this will only affect the BoundaryElement itself, and the attached property value continues to propagate to its children. AddOwner effectively replaces the property at the BoundaryElement so that the original doesn't exist to inherit from.

nmclean
  • 7,564
  • 2
  • 28
  • 37
  • I have tried this and it does not seem to achieve what I want. Strange enough, the property inheritance stops at the BoundaryElement, so it does not have the property set. However, even with Inherits=false in the AddOwer-call, the property value is propagated to children - this time the default value from the BoundaryElement. – user1211286 Dec 04 '13 at 14:58
  • @user1211286 Okay, I did some more testing. Unfortunately there doesn't seem to be any rhyme or reason to when it works and when it doesn't. For example, it works with a TextBox child, but wrap the TextBox in a Grid and it propagates through both. – nmclean Dec 04 '13 at 16:06
  • I think that the reason that you see that problem is because here, you have just shared the original definition of the property. To change the original values, I believe that you will need to set the `FrameworkPropertyMetadataOptions` object as shown in my example (to whatever values suit you). You can find the possible values in the [FrameworkPropertyMetadataOptions Enumeration](http://msdn.microsoft.com/en-us/library/system.windows.frameworkpropertymetadataoptions(v=vs.110).aspx) page on MSDN. – Sheridan Dec 04 '13 at 16:24
  • @Sheridan: As nmclean pointed out, using OverrideMetadata with Inherits false does not solve the problem. It does not prevent further inheritance. Or is your suggestion something else? – user1211286 Dec 05 '13 at 15:23
  • No, my idea was to replace the original `FrameworkPropertyMetaDataOptions` with the same set of options, but replacing the `FrameworkPropertyMetaDataOptions.Inherits` value with the `FrameworkPropertyMetaDataOptions.OverridesInheritanceBehavior` value. I can't tell you whether this will work or not... it was just a suggestion. – Sheridan Dec 05 '13 at 15:34
  • I don't understand what that should look like. If you could provide a code snippet, that that would help. I also noticed, that your example was a dependency property, while I was talking about an _attached_ property, so the boundary control I am looking at is not a subclass of the property hoster. – user1211286 Dec 05 '13 at 15:40
0

There is one possibility that I know about. It's not really what you're after, but you can use the DependencyProperty.OverrideMetadata Method to override the PropertyMetadata of the DependencyProperty in an extended control:

public class SomeControl : OriginalControl
{
    static SomeControl()
    {
        OriginalControl.SomeProperty.OverrideMetadata(typeof(SomeControl), 
            new FrameworkPropertyMetadata(defaultValue, 
            FrameworkPropertyMetadataOptions.OverridesInheritanceBehavior));
    }
}

Apart from this, (I could be wrong but) I don't think that you'll be able to achieve your goal... WPF just wasn't designed like that.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • From what I understand in the documentation, OverridesInheritanceBehavior is intended to propagate/inherit values even further across boundaries that would normally prevent inheritance (like Frame specifying SkipAllNext). – user1211286 Dec 04 '13 at 15:00
0

The explanation for this is a bit sad and complex, but fortunately the solution is very easy. You do not need to use AddOwner(...).

To block inheritance when using DependencyProperty.OverrideMetadata(...), you cannot use the FrameworkPropertyMetadata constructor to specify the FrameworkPropertyMetadataOptions flag "OverridesInheritanceBehavior". The reason is that, for certain FPMO flags, the default FrameworkPropertyMetadata.Merge(...) function only processes flags that have been explicitly modified via the FPM property setters.

The Inherits flag/property is another one that also has this behavior, and also you need to clear it from any base metadata. So the only way to have Merge(...) clear the value that results (after merging with the base property metadata) is to, again, explicitly set it via its FPM setter.

It's confusing and unfortunately a very unnatural/unexpected/non-obvious design, so here is an example. Assume SomeDependencyProperty is a bool property, and the base metadata specifies a DefaultValue of false, and it has DP inheritance enabled. You want to override the default value to be true on your derived class MyType.

So the whole issue discussed on this page/question is that the following doesn't work:

SomeDependencyProperty.OverrideMetadata(typeof(MyType), 
    new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.OverridesInheritanceBehavior));

As discussed above, there are two problems with the preceding, but the more obvious one is that, even if the FPMO.OverridesInheritanceBehavior works as expected, you haven't un-set the FPMO.Inherits flag itself. Obviously, there's no way to unset a bit flag by ORing together bit-flags that can only be asserted. So essentially, after the Merge, you would be overriding the inheritance behavior--but with inheritance still being enabled (due to Merge). And finally, with inheritance still ultimately enabled, your desired DefaultValue will essentially have no effect.

Solution

Instead, use the property setters, as shown in the following, which works to block inheritance and, thus, allow your modified DefaultValue to take effect:

SomeDependencyProperty.OverrideMetadata(typeof(MyType),
    new FrameworkPropertyMetadata(true)
    {
        Inherits = false,
        OverridesInheritanceBehavior = true,
    });

reference: Merge(...) @ FrameworkPropertyMetadata.cs

Glenn Slayden
  • 17,543
  • 3
  • 114
  • 108