65.9K
CodeProject 正在变化。 阅读更多。
Home

程序化类型安全绑定

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2012 年 9 月 18 日

CPOL

4分钟阅读

viewsIcon

14835

在代码中以类型安全的方式使用"{Binding X.Y.Z}"。

这篇文章是关于什么的?

考虑以下嵌套成员表达式

this.Person.Employer.Address  

如果可以这样写,岂不是很棒

Observe(() => this.Person.Employer.Address, address => UpdateAddress(address)); 

意思是每次表达式更改时,UpdateAddress 都会自动调用?

请注意,更改可能来自任何级别,从 PersonAddress 本身。 这正是 XAML 绑定所做的事情,本文解释了如何在代码中执行此操作。

我正在使用 Silverlight - 我希望它也能在 WPF 和 WinRT 中工作,但我还没有尝试过。

为什么这很重要?

在使用 MVVM 范例进行编码时,需要实现针对 XAML 编写的视图绑定的视图模型。

我发现这些视图模型本身经常需要绑定到其他源。 有两种常见情况

  1. 视图由需要相应子视图模型的子视图组成,并且外部视图模型需要收到此类子视图模型中更改的通知。
  2. 包含实际数据并支持视图模型的模型本身具有“视图模型”特征 - 我将通过一个示例来解释我的意思

以 RIA 服务为例。 使用此框架,(某些)模型将是自动生成的实体类。 它们实际上可以在简单的情况下扮演视图模型本身的角色,但假设我们有一个单独的视图模型。 当用户交互或加载操作更新此类实体时,有时必须通知视图模型进行相应更新。 请看下图

许多实体类通常是给定的:它们只是密切地模拟数据的组织方式。 现在想象一下,如果地址与当前登录用户所指的房屋相同,则视图应显示一个突出的视觉效果。 要实现这一点,需要跟踪视图模型中此成员表达式的更改

this.Person.Employer.Address

更改很可能由以下原因触发

  • 用户通过编辑文本或
  • 用户通过拒绝当前更改或
  • 加载操作的完成。

连接所有这些可能非常不同的代码路径以触发视图模型中的某些更新方法既繁琐又容易出错。 为什么我们不能像在 XAML 绑定中那样观察属性路径? 我们想要写的是类似 "{Binding Person.Employer.Address}" 的东西。

它是如何完成的?

关键是以编程方式使用 Binding 对象。 这意味着我们工具的行为将与 XAML 绑定的行为相同,并且它将在完全相同的情况下工作。

由于 Binding 对象期望属性路径作为字符串文字,并且我们想要使用类型安全和名称安全的 lambda 表达式,因此第一步是使用一种工具来从这些表达式生成属性路径。

public class RootedPropertyPath<T>
{
    public Object Target { get; set; }
    public String Path { get; set; }

    public Binding ToBinding(BindingMode mode = BindingMode.TwoWay)
    {
        return new Binding(Path) { Source = Target, Mode = mode };
    }

    public static implicit operator System.Windows.PropertyPath(RootedPropertyPath<T> self)
    {
        return new System.Windows.PropertyPath(self.Path);
    }

    public static implicit operator String(RootedPropertyPath<T> self)
    {
        return self.Path;
    }
}

public static class RootedPropertyPath
{
    public static RootedPropertyPath<T> Create<T>(Expression<Func<T>> expr)
    {
        Expression currentExpression = expr.Body;

        List<String> lst = new List<String>();

        ConstantExpression ce;

        while (true)
        {
            ce = currentExpression as ConstantExpression;

            var me = currentExpression as MemberExpression;

            if (ce != null) break;

            if (me == null)
                throw new Exception(String.Format(
                    "Unexpected expression type {0} in lambda.", expr.GetType()));

            lst.Add(me.Member.Name);

            currentExpression = me.Expression;
        }

        lst.Reverse();

        return new RootedPropertyPath<T>() { Path = String.Join(".", lst), Target = ce.Value };
    }
}

请注意,RootedPropertyPath<T> 已经比类型安全的属性路径稍微多一些:我们还直接从表达式中收集基本对象,即绑定的“目标” - 因此名称中的“rooted”。

现在可以使用以下方式构造路径

var rootedPath = RootedPropertyPath.Create(() => this.Person.Employer.Address) 

然后

var binding = rootedPath.ToBinding() 

为我们提供了观察嵌套成员表达式所需的绑定。

Create 方法基本上由用于以类型安全和名称安全的方式实现 INotifyPropertyChanged 的常见技巧组成。

下一步是绑定一些东西。 为了做到这一点,我们需要一个正确类型的 DependencyProperty 并将其绑定到我们拥有的绑定。 这就是以下工具的用途

public static class ObservableDependencyValue
{
    public static ObservableDependencyValue<T> Create<T>(
       Expression<Func<T>> expr, BindingMode mode = BindingMode.OneWay)
    {
        return new ObservableDependencyValue<T>().Bind(expr, mode);
    }
}

public class ObservableDependencyValue<T> : DependencyObject
{
    #region T ObservableDependencyValue<T>.Value = default(T)
    #region Boilerplate

    public T Value
    {
        get { return (T)GetValue(ValueProperty); }
        set { SetValue(ValueProperty, value); }
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(T), 
        typeof(ObservableDependencyValue<T>), new PropertyMetadata(default(T), StaticHandleValueChanged));

    static void StaticHandleValueChanged(DependencyObject self, DependencyPropertyChangedEventArgs args)
    {
        ((ObservableDependencyValue<T>)self).HandleValueChanged((T)args.OldValue, (T)args.NewValue);
    }

    #endregion

    void HandleValueChanged(T oldValue, T value)
    {
        Notify();
    }
    #endregion

    public ObservableDependencyValue()
    {
    }

    public ObservableDependencyValue(BindingBase bindingBase)
    {
        Bind(bindingBase);
    }

    public ObservableDependencyValue<T> Bind(BindingBase bindingBase)
    {
        BindingOperations.SetBinding(this, ValueProperty, bindingBase);

        return this;
    }

    public ObservableDependencyValue<T> Bind(Expression<Func<T>> expr, 
           BindingMode mode = BindingMode.OneWay)
    {
        var path = RootedPropertyPath.Create(expr);

        return Bind(new Binding(path.Path) { Source = path.Target, Mode = mode });
    }

    public void Notify()
    {
        if (ValueChanged != null) ValueChanged(Value);
    }

    public event Action<T> ValueChanged;
}

ObservableDependencyProperty<T> 仅存在于其一个属性“Value”中,该属性可以绑定到任意绑定并报告更改。 为了方便起见,绑定的创建已经内置到类中 - 我们只需要在 ObservableDependencyValue 的工厂方法中传递成员表达式本身。

我们现在差不多完成了。 如果我们像这样编写视图模型

public class PersonViewModel : OurViewModelBase
{
    public PersonViewModel()
    {
        var observableDependencyProperty = ObservableDependencyValue.Create(() => 
                       this.Person.Employer.Address);

我们有一个对象,该对象具有在成员表达式更改时触发的事件。 这可以直接使用

observableDependencyProperty.ValueChanged += address => UpdateAddress(address);

但有一个主要的缺陷:我们必须确保我们的 observableDependencyProperty 将与我们的视图模型一样长的时间存在。 因此,我们需要在我们的视图模型中存储一个引用

    observableDependencyPropertyReference = observableDependencyProperty;
  }

  Object observableDependencyPropertyReference;
}

这有点不幸 - 但由于为视图模型提供自定义公共基类是一种常见的做法,我建议在那里添加一些东西来注册此类对象

public class OurViewModelBase
{
    ...

    protected void Observe<T>(Expression<Func<T>> expr, Action<T> changed)
    {
        var odv = ObservableDependencyValue.Create(expr);
        odv.ValueChanged += changed;
        lifetimeObjects.Add(odv);
    }

    List<Object> lifetimeObjects = new List<Object>();

    ...
}

这就是承诺的语法和本文的结论 - 我希望您觉得这项技术有用。

© . All rights reserved.