Reputation: 2074
My overall goal is to create a TwoWay Attached DependencyProperty (or OneWayToSource) that always keeps it's bound source updated to a specific value. In my real world scenario this is a not-constant object that depends on the object it's attached to.
My sample involves the following models and view:
public class ViewModel : ViewModelBase
{
public ViewModel()
{
firstContainer = new Container();
otherContainer = new Container();
TestContainer = firstContainer;
SwitchContainersCommand = new DelegateCommand(SwitchContainers);
}
private Container firstContainer;
private Container otherContainer;
public Container TestContainer
{
get { return testContainer; }
set { Set(ref testContainer, value); }
}
private Container testContainer;
public ICommand SwitchContainersCommand { get; }
private void SwitchContainers()
{
if (TestContainer == firstContainer)
TestContainer = otherContainer;
else
TestContainer = firstContainer;
}
}
public class Container : ViewModelBase
{
public object TestTarget
{
get { return testTarget; }
set { Set(ref testTarget, value); }
}
private object testTarget = new SampleValue();
}
public class SampleValue
{
public override string ToString()
{
return "unprovided value";
}
}
View:
<Window>
<Window.DataContext>
<local:ViewModel/>
</Window.DataContext>
<StackPanel local:ProvideToSource.Target="{Binding TestContainer.TestTarget, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<TextBlock><Run Text="ToString: "/><Run Text="{Binding TestContainer.TestTarget}"/></TextBlock>
<Button Command="{Binding SwitchContainersCommand}" HorizontalAlignment="Left">
<TextBlock Text="Switch"/>
</Button>
</StackPanel>
</Window>
Notably, we have an attached property local:ProvideToSource.Target
, which is binding to TestContainer.TestTarget
bi-directionally.
Now, the code for ProvideToSource
isn't too complicated. On Coerce Value, it attempts to set the property to it's desired value instead. This works just fine, on the initial run.
public static class ProvideToSource
{
private const string Target = nameof(Target);
public static DependencyProperty TargetProperty = DependencyProperty.RegisterAttached(nameof(Target), typeof(object), typeof(ProvideToSource), new PropertyMetadata(null, null, CoerceValue));
public static object GetTarget(DependencyObject dependent)
{
return dependent.GetValue(TargetProperty);
}
public static void SetTarget(DependencyObject dependent, object value)
{
dependent.SetValue(TargetProperty, value);
}
private static readonly object TestValue = new ProvidedObject();
private static object CoerceValue(DependencyObject dependent, object value)
{
if (value != TestValue)
{
dependent.SetValue(TargetProperty, TestValue);
}
return TestValue;
}
}
public class ProvidedObject
{
public override string ToString()
{
return "provided me";
}
}
Ideally, when the binding gets set to a new source (that isn't up to date), it should go and update the source to be the value we desire. However, it's only doing that on the initial creation.
When the app begins and the window displays, the text says the desired "provided me"; however, when the button is clicked, and the TestContainers are swapped, the binding re-evaluates it's Coerce Value (as it detects a change), but when SetValue
is called, the backing property is not updated, and the text displays "unprovided value"
Is there a reason for this behavior, and how can I get around it? I have tried many things; however, I simply can't seem to get the source value to be successfully set from the attached property after the binding has been initialized. Note that I have tried this with and without PropertyChangedCallback and I can't seem to get that to help with the problem either.
Additionally, explicitly calling BindingOperations.GetBindingExpression(dependent, TargetProperty)?.UpdateSource();
within the CoerceValue or PropertyChanged callbacks also fails to execute.
Upvotes: 2
Views: 962
Reputation: 1
I extended Marko elegant solution to extension class. Example tested in .NET 4.7.2 and it works like a charm.
public static class BindingExtensions
{
private delegate object UpdateSourceDelegate(BindingExpression expression, object value);
private static readonly UpdateSourceDelegate s_updateSource;
static BindingExtensions()
{
var mi = typeof(BindingExpression).GetMethod("UpdateSource", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new[] { typeof(object) }, null);
Debug.Assert(mi != null, "Can't find UpdateSource");
s_updateSource = (UpdateSourceDelegate)Delegate.CreateDelegate(typeof(UpdateSourceDelegate), mi);
}
public static object UpdateSource(this BindingExpression expression, object value)
{
return s_updateSource(expression, value);
}
}
So I can use expr.UpdateSource(newValue) in such way for my CalendarDialog implementation:
public static readonly DependencyProperty SelectedDateProperty =
DependencyProperty.Register(nameof(SelectedDate),
typeof(DateTime?),
typeof(CalendarDialog),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
(d, e) => ((CalendarDialog)d).OnSelectedDateChanged((DateTime?)e.NewValue, (DateTime?)e.OldValue),
(d, value) => ((CalendarDialog)d).CoerceSelectedDate(value)));
private bool IsSourseNeedsUpdate { get; set; }
public DateTime? SelectedDate
{
get => (DateTime?)GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
private object CoerceSelectedDate(object baseValue)
{
if (baseValue is DateTime value && SelectedDate is DateTime selectedDate)
{
if (value != selectedDate)
{
baseValue = value.Date + selectedDate.TimeOfDay;
IsSourseNeedsUpdate = true;
}
}
return baseValue;
}
private void OnSelectedDateChanged(DateTime? newValue, DateTime? oldValue)
{
if (newValue != null && newValue != oldValue && IsSourseNeedsUpdate)
{
var expr = GetBindingExpression(SelectedDateProperty);
if (expr != null && expr.ResolvedSource != null && expr.ResolvedSourcePropertyName != null)
{
expr.UpdateSource(newValue);
IsSourseNeedsUpdate = false;
}
}
Debug.Assert(IsSourseNeedsUpdate == false);
IsSourseNeedsUpdate = false;
}
Note: Source updating invokes twice, but it is OK in this case.
Upvotes: 0
Reputation: 11
Not much time to write so i will be brief in case someone face this issue:
On DependencyObject add property for unCoerceValue
If (value != unCoerceValue And value coerced(amended?)), inside the if:
Set unCoerceValue property
Force UpdateTarget and pass through Coerce method through until returning the coerced value
Set the dp to the Coerced value
Exit the if and watch out for stackOverFlow!
This will make the target value to be set twice if value have to be coerce(amended?) and the second time it will trigger and update the source just because/if binding mode is twoway or one way to the source.
example below:
public class Rotator : whatever
{
//dp for CurrentSelectedIndexProperty....
//dp for ItemsSourceProperty....
private int UnCoercedCurrentSelectedIndex { get; set; }
private static object CoerceCurrentSelectedIndex(DependencyObject d, object baseValue)
{
Rotator rotator = d as Rotator;
int newIndex = baseValue as int? ?? 0;
int itemCount = rotator.ItemsSource?.Cast<object>()?.Count() ?? 0;
int realIndex = Math.Abs(newIndex) % itemCount;
if (rotator.UnCoercedCurrentSelectedIndex != newIndex && newIndex != realIndex)
{
rotator.UnCoercedCurrentSelectedIndex = newIndex;
rotator.GetBindingExpression(CurrentSelectedIndexProperty)?.UpdateTarget();
rotator.CurrentSelectedIndex = realIndex;
}
return realIndex;
}
in xaml:
<Rotator CurrentSelectedIndex="{Binding somePropertyInVM,Mode=TwoWay}" />
Upvotes: 0
Reputation: 2365
I ran into a similar issue, that BingindExpression
would not do UpdateSource()
within OnCoerce callback. But I desperately needed the source value to be in sync after coercion.
My solution ended up being this:
private static object OnCoerceValue(DependencyObject dobj, object value)
{
NumericUpDown nupd = (NumericUpDown)dobj;
decimal origValue = (decimal)value, newValue = origValue;
if (newValue < nupd.Minimum) newValue = nupd.Minimum;
else if (newValue > nupd.Maximum) newValue = nupd.Maximum;
// when setting nupd.Value and value is not within min/max
// 1. Binding.Source is updated with the out of range value
// 2. Coercion happens afther binding source update and nupd gets set to the correct value, but source does not get updated with coerced value
// 3. BindingExpression UpdateSource() does nothing here, the only way to get source back in sync is via reflection.
if (newValue != origValue)
{
// sync source via reflection
var be = nupd.GetBindingExpression(NumericUpDown.ValueProperty);
if (be != null && be.ResolvedSource != null && be.ResolvedSourcePropertyName != null)
{
var field = be.GetType().GetField("_sourceType", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
Type srcType = (Type)field!.GetValue(be)!;
// force update source
var updsrc = be.GetType().GetMethod("UpdateSource", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, new Type[] { typeof(object) });
updsrc!.Invoke(be, new object[] { Convert.ChangeType(newValue, srcType) }); // ! , if is null, I want it to crash, not use invalid data
}
}
return newValue;
}
I am using c# 8 nullable features with the !
at updsrc!.Invoke()
and (Type)field!.GetValue(be)!;
.
Also, because my Value
property is of type decimal
, but you can bind to any numeric - int, short, byte
, etc. I had to do an extra step, and get the source type via reflection and cast the new value to the source type.
If your value type is constant in this regard, you can skip the field "_sourceType"
reflection.
I'm using reflection to get access to an internal overloaded method UpdateSource(object value)
.
I think this is a very elegant solution (to a problem that shouldn't exist in the first place). Although reflection is needed to get access to private fields and methods, I doubt Microsoft is ever going to change the implementation. WPF has been dead in the waters for over 10 years and Microsoft seems to invest in WinUI 3 only these days...
Upvotes: 0
Reputation: 2074
I believe I finally found the best solution I can. One of my biggest gaps in knowledge was about the Dispatcher system; while I don't think being on the wrong thread caused all of the issues I encountered, I'd rather be safe than sorry.
The following section code defines a two-way binding property that will always push it's desired value back to the bound source property - if the source notifies the binding of an update, the binding will circle back and update the source back to what the desired value should be - effectively providing a reliable method of passing arbitrary information back from the View to the ViewModel.
On the initial run, the CoerceValueCallback
will initialize the desired value of the property, and store it in the private Cache dependency property, and force the binding to update to that value. On all subsequent runs, the PropertyChangedCallback
will check if the property is being changed to anything that isn't the desired value, and will force the binding back to it's desired value (loaded from the Cache dependency property).
public static class ProvideToSource
{
private const string Target = nameof(Target);
public static DependencyProperty TargetProperty = DependencyProperty.RegisterAttached(nameof(Target), typeof(object), typeof(ProvideToSource), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, ValueChanged, CoerceValue, true, UpdateSourceTrigger.Explicit));
public static object GetTarget(DependencyObject dependent)
{
return dependent.GetValue(TargetProperty);
}
public static void SetTarget(DependencyObject dependent, object value)
{
dependent.SetValue(TargetProperty, value);
}
private const string Cache = nameof(Cache);
private static DependencyProperty CacheProperty = DependencyProperty.RegisterAttached(nameof(Cache), typeof(object), typeof(ProvideToSource));
private static object CoerceValue(DependencyObject dependent, object value)
{
if (dependent.GetValue(CacheProperty) == null)
{
value = GetOrCreateCachedValue(dependent);
dependent.Dispatcher.Invoke(() => UpdateValueAndBindings(dependent, value));
}
return value;
}
private static void ValueChanged(DependencyObject dependent, DependencyPropertyChangedEventArgs args)
{
var cachedValue = GetOrCreateCachedValue(dependent);
if (args.NewValue != cachedValue)
{
dependent.Dispatcher.Invoke(() => UpdateValueAndBindings(dependent, cachedValue));
}
}
private static void UpdateValueAndBindings(DependencyObject dependent, object value)
{
var bindingExpression = BindingOperations.GetBindingExpression(dependent, TargetProperty);
if (bindingExpression != null && bindingExpression.Status != BindingStatus.Detached)
{
bindingExpression?.UpdateTarget(); //This call seems out of place but is required
SetTarget(dependent, value);
bindingExpression?.UpdateSource();
}
else
{
SetTarget(dependent, value);
}
}
private static object GetOrCreateCachedValue(DependencyObject dependent)
{
var item = dependent.GetValue(CacheProperty);
if (item == null)
{
item = new ProvidedObject(); // Generate item here
dependent.SetValue(CacheProperty, item);
}
return item;
}
}
Using this code in place for the TargetProperty
definition in the question, will make sure that the current ViewModel.TestContainer.TestTarget
is always kept in sync to the instance of ProvidedObject
.
The initial set is done in the CoerceValueCallback
to ensure the initial value gets set, as the PropertyChangedCallback
may not get set on the initial run through the dependency property if it's bound to a null value.
The subsequent sets are done in the PropertyChangedCallback
to ensure that the dependency property already considers itself to be fully updated before attempting to immediately change it back, rather than attempting to force it back mid-coercion. Additionally, as UpdateTarget()
is required to be called before we can UpdateSource()
(due to some unusual behavior when the original source object is replaced by a different source object), we rely on PropertyChangedCallback
filtering out the effects of that call, as it won't think the binding is changing value yet, and does not cause infinite recursion - calling UpdateTarget()
from within the CoerceValueCallback
causes an infinite recursion.
The code can be modified for any distinct type of dependency property, but object
has less boilerplate.
Upvotes: 1