题
我们的领域模型中有很多不可变的值对象,其中一个例子是由纬度、经度和高度定义的位置。
/// <remarks>When I grow up I want to be an F# record.</remarks>
public class Position
{
public double Latitude
{
get;
private set;
}
// snip
public Position(double latitude, double longitude, double height)
{
Latitude = latitude;
// snip
}
}
允许编辑位置的明显方法是构建一个具有吸气剂的 ViewModel 和 设置器,以及用于提取经过验证的不可变位置实例的 ToPosition() 方法。虽然这个解决方案没问题,但它会导致大量重复的代码,尤其是 XAML。
所讨论的值对象由三到五个属性组成,这些属性通常是 X、Y、Z 和一些辅助内容的某种变体。鉴于此,我考虑创建三个 ViewModel 来处理各种可能性,其中每个 ViewModel 需要公开每个属性值的属性以及要为每个标签显示的描述(例如,“纬度”)。
更进一步,我似乎可以将其简化为一个通用的 ViewModel,它可以处理 N 个属性并使用反射将所有内容连接起来。类似于属性网格,但用于不可变对象。属性网格的一个问题是我希望能够更改外观,以便可以拥有标签和文本框,例如:
Latitude: [ 32 ] <- TextBox
Longitude: [ 115 ]
Height: [ 12 ]
或者将其放入 DataGrid 中,例如:
Latitude | Longitude | Height
32 115 12
所以我的问题是:
你能想出一个优雅的方法来解决这个问题吗?是否有任何库可以执行此操作或有类似的文章?
我主要是在寻找:
- 尽量减少代码重复
- 轻松添加新的值对象类型
- 可以通过某种验证进行扩展
解决方案
我在研究相同情况下可能的选择时发现了这个老问题。我想我应该更新它,以防其他人偶然发现它:
另一种选择(由于 .Net 4 尚未推出,当 Paul 提供他的解决方案时不可用)是使用相同的策略,但不是使用 CustomTypeDescriptors 实现它,而是使用泛型、动态对象和反射的组合来实现相同的效果。
在本例中,您定义一个类
class Mutable<ImmutableType> : DynamicObject
{
//...
}
它的构造函数采用不可变类型的实例和委托,该委托从字典中构造它的新实例,就像保罗的回答一样。然而,这里的区别在于,您重写了 TryGetMember 和 TrySetMember 以填充最终将用作构造函数委托的参数的内部字典。您使用反射来验证您接受的唯一属性是否是在 ImmutableType 中实际实现的属性。
就性能而言,我敢打赌 Paul 的答案更快,并且不涉及动态对象,众所周知,动态对象会让 C# 开发人员陷入困境。但这个解决方案的实现也稍微简单一些,因为类型描述符有点神秘。
这是所请求的概念验证/示例实现:
其他提示
自定义类型描述符可以用来解决这个问题。在绑定到 Position 之前,您的类型描述符可能会启动,并提供 get 和 set 方法来临时构建值。当提交更改时,它可以构建不可变对象。
它可能看起来像这样:
DataContext = new Mutable(position,
dictionary => new Position(dictionary["lattitude"], ...)
);
您的绑定仍然可以如下所示:
<TextBox Text="{Binding Path=Lattitude}" />
因为可变对象将“假装”拥有像 Lattitude 这样的属性,这要归功于它的 TypeDescriptor。
或者,您可以在绑定中使用转换器并提出某种约定。
您的 Mutable 类将采用当前的不可变对象,以及 Func<IDictionary, object>
这允许您在编辑完成后创建新的不可变对象。您的 Mutable 类将使用类型描述符,这将创建 PropertyDescriptors,在设置时创建新的不可变对象。
有关如何使用类型描述符的示例,请参见此处:
http://www.paulstovell.com/editable-object-adapter
编辑:如果您想限制创建不可变对象的频率,您还可以查看 BindingGroups 和 IEditableObject,您的 Mutable 也可以实现它们。
你能想出一个优雅的方法来解决这个问题吗?
老实说,你只是绕着问题跳舞,但不提及问题本身;)。
如果我正确地猜出了您的问题,那么 MultiBinding 和 IMultiValueConverter 的组合应该可以解决问题。
HTH。
附:顺便说一句,您拥有不可变的类实例,而不是值对象。使用值对象(由 struct
关键字)无论是否有二传手,你都会跳舞更多:)。