UpdateSource 无法在 CoerceValueCallback 中工作

2024-01-10

我的总体目标是创建一个 TwoWay Attached DependencyProperty (或 OneWayToSource),始终将其绑定源更新为特定值。在我的现实世界场景中,这是一个不恒定的对象,取决于它所附加的对象。

我的示例涉及以下模型和视图:

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>

值得注意的是,我们有一个附加财产local:ProvideToSource.Target,它绑定到TestContainer.TestTarget双向。

现在,代码为ProvideToSource并不太复杂。在强制值上,它会尝试将属性设置为其所需的值。在初次运行时,这工作得很好。

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";
  }
}

理想情况下,当绑定设置为新源(不是最新的)时,它应该将源更新为我们想要的值。 然而,它只是在初始创建时这样做。

当应用程序启动并显示窗口时,文本显示所需的“由我提供”;但是,当单击按钮并交换 TestContainer 时,绑定会重新评估它的强制值(因为它检测到更改),但是当SetValue被调用,后备属性未更新,文本显示“未提供的值”

这种行为有原因吗?我该如何解决它?我尝试过很多事情;但是,在初始化绑定后,我似乎无法从附加属性中成功设置源值。请注意,我已经在使用和不使用 PropertyChangedCallback 的情况下尝试过此操作,但我似乎也无法帮助解决问题。

此外,显式调用BindingOperations.GetBindingExpression(dependent, TargetProperty)?.UpdateSource();CoerceValue 或 PropertyChanged 回调中也无法执行。


我相信我终于找到了最好的解决方案。 我最大的知识空白之一是关于调度程序系统;虽然我不认为我遇到的所有问题都是由错误的线程引起的,但我宁愿安全也不愿后悔。

以下部分代码定义了一个双向绑定属性,该属性始终将其所需值推回到绑定的源属性 - 如果源通知绑定更新,则绑定将循环并将源更新回所需值应该 - 有效地提供一种将任意信息从 View 传递回 ViewModel 的可靠方法。

在初次运行时,CoerceValueCallback将初始化属性的所需值,并将其存储在私有 Cache 依赖项属性中,并强制绑定更新为该值。在所有后续运行中,PropertyChangedCallback将检查该属性是否被更改为任何不是所需值的值,并将强制绑定回其所需值(从缓存依赖项属性加载)。

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;
  }
}

使用此代码就位TargetProperty问题中的定义将确保当前ViewModel.TestContainer.TestTarget始终与实例保持同步ProvidedObject.

初始设置是在CoerceValueCallback确保初始值被设置,因为PropertyChangedCallback如果绑定到 null 值,则在首次运行依赖项属性时可能不会设置。

后续组在中完成PropertyChangedCallback确保依赖属性在尝试立即将其更改回来之前已经认为自己已完全更新,而不是尝试在强制过程中将其强制返回。另外,如UpdateTarget()在我们可以之前需要调用UpdateSource()(由于当原始源对象被不同的源对象替换时出现一些异常行为),我们依赖PropertyChangedCallback过滤掉该调用的影响,因为它不会认为绑定正在改变值,并且不会导致无限递归 - 调用UpdateTarget()从内部CoerceValueCallback导致无限递归。

可以针对任何不同类型的依赖属性修改代码,但是object样板较少。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

UpdateSource 无法在 CoerceValueCallback 中工作 的相关文章

随机推荐