Yes, there is a clean way to "make a read-only
DependencyProperty reflect the value of another property," but it may require a pretty fundamental shift in the overall property programming model of your app. In short, instead of using the
DependencyPropertyKey to push values into the property, every read-only
DependencyProperty can have a CoerceValue callback which builds its own value by pulling all the source values it depends on.
In this approach, the 'value' parameter that's passed in to
CoerceValue is ignored. Instead, each DP's
CoerceValue function recalculates its value "from scratch" by directly fetching whatever values it needs from the
DependencyObject instance passed in to
CoerceValue (you can use
dobj.GetValue(...) for this if you want to avoid casting to the owner instance type).
Try to suppress any suspicion that ignoring the value supplied to
CoerceValue may be wasting something. If you adhere to this model, those values will never be useful and the overall work is the same or less than a "push" model because source values that haven't changed are, as always, cached by the DP system. All that's changed is who's responsible for the calculation and where it's done. What's nice here is that calculation of each DP value is always centralized in one place and specifically associated with that DP, rather than strewn across the app.
You can throw away the
DependencyPropertyKey in this model because you'll never need it. Instead, to update the value of any read-only DP you just call
InvalidateValue on the owner instance, indicating the desired DP. This works because those two functions don't require the DP key, they use the public
DependencyProperty identifier instead, and they're public functions, so any code can call them from anywhere.
As for when and where to put these
InvalidateValue calls, there are two options:
- Eager: Put an
InvalidateValue call for the (target) DP in the
PropertyChangedCallback of every (source) DP that's mentioned in the (target) DP's
- Lazy: Always call
CoerceValue on the DP immediately prior to fetching its value.
It's true that this method is not so XAML-friendly, but that wasn't a requirement of the OPs question. Considering, however, that in this approach you don't ever even need to fetch or retain the
DependencyPropertyKey at all, it seems like it might one of the sleekest ways to go, if you're able to reconceive your app around the "pull" semantics.
In a completely separate vein, there's yet another solution that may be even simpler:
INotifyPropertyChanged on your
DependencyObject and use CLR properties for the read-only properties, which will now have a simple backing field. Yes, the WPF binding system will correctly detect and monitor both mechanisms--
INotifyPropertyChanged--on the same class instance. A setter, private or otherwise, is recommended for pushing changes to this read-only property, and this setter should check the backing field to detect vacuous (redundant) changes, otherwise raising the old-style CLR
For binding to this read-only property, either use the owner's
OnPropertyChanged overload (for self-binding) to push in the changes from DPs, or, for binding from arbitrary external properties, use
System.ComponentModel.DependencyPropertyDescriptor.FromProperty to get a
DependencyPropertyDescriptor for the relevant souce DPs, and use its
AddValueChanged method to set a handler which pushes in new values.
Of course for non-DP properties or non-
DependencyObject instances, you can just subscribe to their
INotifyPropertyChanged event to monitor changes that might affect your read-only property. In any case, no matter which way you push changes into the read-only property, the event raised by its setter ensures that changes to the read-only property correctly propagate onwards to any further dependent properties, whether WPF/DP, CLR, data-bound or otherwise.