【问题标题】:How can I update the source of a binding when cancelling update in DependencyProperty's CoerceValueCallback?在 DependencyProperty 的 CoerceValueCallback 中取消更新时如何更新绑定源?
【发布时间】:2012-11-10 02:20:21
【问题描述】:

我有一个控件(在下面的示例中名为 GridView)和一个视图模型,它们通过 2 路绑定对其 SelectedValue 属性进行绑定。我想通过使用CoerceValueCallback 来禁止GridView 控件中该属性的某些值。

当绑定将该无效值(在此示例中为 42)推送到 DependencyProperty 时,CoerceValue 方法会丢弃该值,并且控件会按预期保留其先前的值。不幸的是,由于DependencyProperty 没有更改绑定,因此不会更新视图模型中的源值。

取消属性值更改时,如何用控件中的值更新视图模型中的属性?

class ViewModel : INotifyPropertyChanged
{
    public int? SelectedValue
    {
        get { return _selectedValue; }
        set
        {
            _selectedValue = value;
            RaisePropertyChanged("SelectedValue");
        }
    }
    private int? _selectedValue;

    public event PropertyChangedEventHandler PropertyChanged;
    private void RaisePropertyChanged(string name)
    {
        var propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(name));
        }
    }
}

class GridView : FrameworkElement
{
    public object SelectedValue
    {
        get { return (object)GetValue(SelectedValueProperty); }
        set { SetValue(SelectedValueProperty, value); }
    }

    public static readonly DependencyProperty SelectedValueProperty =
        DependencyProperty.Register("SelectedValue", typeof(object), typeof(GridView),
            new PropertyMetadata(null, null, new CoerceValueCallback(CoerceValue)));

    private static object CoerceValue(DependencyObject d, object baseValue)
    {
        // forbid value 42 and return previous value
        if (Equals(baseValue, 42))
        {
            return d.GetValue(GridView.SelectedValueProperty);
        }
        return baseValue;
    }
}

    [STAThread]
    static void Main()
    {
        ViewModel vm = new ViewModel();
        GridView grid = new GridView();
        Binding binding =  new Binding("SelectedValue") { Source = vm, Mode = BindingMode.TwoWay };
        grid.SetBinding(GridView.SelectedValueProperty, binding);

        vm.SelectedValue = 12;
        Console.WriteLine("should be 12: {0} = {1}", grid.SelectedValue, vm.SelectedValue);
        grid.SelectedValue = 23;
        Console.WriteLine("should be 23: {0} = {1}", grid.SelectedValue, vm.SelectedValue);
        vm.SelectedValue = 42;
        Console.WriteLine("should still be 23: {0} = {1}", grid.SelectedValue, vm.SelectedValue);
    }

我试过了

    var binding = BindingOperations.GetBindingExpression(d,
                      GridView.SelectedValueProperty);
    binding.UpdateSource();

CoerceValue 方法无济于事。我什至用Dispatcher.CurrentDispatcher.BeginInvoke(updateSource, DispatcherPriority.DataBind); 尝试了前两行,这是在类似的堆栈溢出问题中提出的。

还有其他想法吗?

更新

@Berryl 建议我应该在视图模型中执行该逻辑,我同意他的观点……但我不能。我将在上面的代码中解释我试图简化的完整用例。

GridView 不是我的控件,而是我继承的第 3 方数据网格。当用户编辑记录时,我们会弹出一个窗口,用户在其中编辑记录,然后视图模型刷新数据并使用其 ID 重新选择旧记录。在用户使用网格的过滤功能之前,一切正常。如果对记录的编辑由于过滤器而使该行隐藏,则会发生以下情况:

  1. ValueList 属性中查看模型集
  2. 网格读取该新列表
  3. 网格过滤列表
  4. View Model 使用ValueList 中的记录设置SelectedValue 属性
  5. 网格读取了新的SelectedValue,但将其丢弃,因为它已被过滤掉
  6. 网格的 SelectedValue = 23,视图模型的 SelectedValue = 42
  7. 用户双击所选行(他看到 23)来编辑记录
  8. 在视图模型中调用映射到双击的ICommand
  9. 查看模型启动编辑窗口,其中包含来自其 SelectedValue 属性的记录
  10. 用户编辑记录 42 但认为是 23

【问题讨论】:

  • 似乎逻辑应该在您的视图模型属性设置器中,但您确实明确表示您希望控件处理它...
  • @Berryl 我同意你的看法,看看我的更新。

标签: wpf mvvm binding


【解决方案1】:

我在很久以前的电子邮件中发现了这个解决方案,并认为我应该分享它(稍微清理一下)。这需要当时致电 Telerik 和 Microsoft 的支持。

基本上,我无法从 CoerceValueCallback 取消属性更新的原因是:

  1. 绑定在值被强制转换为依赖属性的逻辑之前更新
  2. CoerceValueCallback 的这种行为是经过设计考虑的

现在是关于如何修复它的长答案:

static GridView()
{
    SelectedItemProperty.OverrideMetadata(typeof(GridView), new FrameworkPropertyMetadata(null, CoerceSelectedItemProperty));
}

// Rewriting SelectedItem coercion logic because of the following issue (we had support calls to both Telerik and Microsoft)
// When
//   - a filter is applied on the grid
//   - user refreshes after modifying a record
//   - the record he has changed would be filtered out by the the grid filter
//   - the view model sets the SelectedItem to the modified record (which is hidden now)
//
// Telerik's coercion method resets the selected item to a visible row but because WPF's
// binding occurs before the coercion method, the view model is not updated.
// Even a call to UpdateSource() on the binding does not work in this case because the binding
// is already updating the target while this happens so it does nothing when you call it.
private static object CoerceSelectedItemProperty(DependencyObject d, object baseValue)
{
    // Call normal Coercion method because we don't want to rewrite Telerik's logic
    // and keep result to return it at the end.
    object returnValue = SelectedItemProperty.GetMetadata(typeof(RadGridView)).CoerceValueCallback(d, baseValue);
    var control = (GridView)d;

    // If coerce returned something other than DependencyProperty.UnsetValue we can use it to push it back to
    // the binding source because it is of the right type and the right value.
    // The only case when we can use control.SelectedItem is when coerce returned UnsetValue otherwise the
    // view model is always one click late.
    object officialValue = returnValue == DependencyProperty.UnsetValue
                                ? control.SelectedItem
                                : returnValue;

    var binding = control.GetBindingExpression(SelectedItemProperty);
    var source = binding.ResolvedSource;
    var property = source.GetType().GetProperty(binding.ResolvedSourcePropertyName);
    property.SetValue(source, officialValue, null);
    return returnValue;
}

【讨论】:

    猜你喜欢
    • 2017-06-13
    • 2011-07-25
    • 2020-01-31
    • 1970-01-01
    • 2011-05-18
    • 2013-05-04
    • 1970-01-01
    • 2011-03-31
    • 1970-01-01
    相关资源
    最近更新 更多