扩展 ObjectPresenter





5.00/5 (3投票s)
本文展示了我们如何扩展 ObjectPresentation 库的行为。
目录
引言
有时,在我们工作中,会发现需要将旧代码用于新的目的。当事情顺利进行时,这可能非常容易。但是,当遇到不兼容或不受支持的问题时,事情可能会变得更加复杂。
在我的情况下,我想使用我旧的 ObjectPresentation 库,来为一些新组件生成测试 GUI。但是,新组件包含 Tuple 对象,这些对象与基本库的行为不兼容。ObjectPresentation
库为所有可以设置的属性生成输入字段。由于 Tuple
属性是只读的,因此没有为它们生成输入字段。
由于我不想让这个小问题阻碍我使用这个库,我决定实现一个扩展来让它正常工作。
ObjectPresentation
库的另一个问题是,任何呈现的引用类型都可以选择折叠或设置为 null
。有时,我们只想显示对象的属性,而不提供设置整个对象值(例如在 ValuePresenter 示例 中的情况)的选项。由于我认为这也很常见,所以我决定也为这种情况实现一个扩展。
在本文中,我们将讨论如何用新行为扩展 ObjectPresentation
库。
背景
ObjectPresentation
库提供的扩展点之一是 定义附加数据模板 的能力。我不得不承认,这个扩展点的主要目的是为一些特殊类型(如示例中的 ColorPicker
)创建简单的数据模板(使用 XAML)。但是,本文中案例的复杂性,促使我更进一步,并实现了一个更复杂的解决方案。在本文中,我们将展示如何通过代码创建通用数据模板解决方案,以扩展 ObjectPresentation
库的行为。
本文假设您熟悉 C# 语言和 WPF 框架。本文使用了 ObjectPresentation
库,因此建议您熟悉 该库的使用。
工作原理
显示 Tuple 属性
从 Tuple 属性获取 Tuple 值
正如我们所提到的,Tuple
属性是只读的。因此,无法通过调用它们的 setter 来设置它们的属性(它们没有 setter)。但是,幸运的是,每种 Tuple
类型都有一个构造函数,它在参数中接受适当的属性值。我们可以为我们的目的使用这个特性。
为了显示 Tuple 属性的输入字段,我们添加了一个控件来处理我们想要的行为。
public class TupleContentControl : ContentControl
{
}
在该控件中,我们添加一个字段来保存 Tuple
属性,并将其设置为控件的 Content
。
public class TupleContentControl : ContentControl
{
public TupleContentControl()
{
_tvvm = new TupleValueViewModel();
Content = _tvvm;
}
}
我们使用一个派生自 ValueViewModel
的类来保存属性,因为我们希望保留原始数据模板的功能(如折叠和展开等),也适用于 Tuple
类型。
为了创建属性输入字段(用于输入数据),我们添加一个字段来保存 Tuple 的类型,并为每个 Tuple 属性(根据 Tuple
的类型)创建一个 PropertyInputValueViewModel
。
#region TupleType
public Type TupleType
{
get { return (Type)GetValue(TupleTypeProperty); }
set { SetValue(TupleTypeProperty, value); }
}
public static readonly DependencyProperty TupleTypeProperty =
DependencyProperty.Register("TupleType", typeof(Type), typeof(TupleContentControl),
new PropertyMetadata(null, new PropertyChangedCallback(onTupleTypeChanged)));
private static void onTupleTypeChanged(DependencyObject o, DependencyPropertyChangedEventArgs arg)
{
TupleContentControl tcc = o as TupleContentControl;
if (tcc != null)
{
tcc.createTupleProperties();
}
}
#endregion
private void createTupleProperties()
{
if (TupleType != null)
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm == null || vvm.IsEditable)
{
_tvvm.SubFields.Clear();
foreach (var prop in TupleType.GetProperties())
{
var newProp = new PropertyInputValueViewModel(prop)
{
DataTemplates = vvm != null ? vvm.DataTemplates : null,
AutoGenerateCompatibleTypes = vvm != null ? vvm.AutoGenerateCompatibleTypes : false,
KnownTypes = vvm != null ? vvm.KnownTypes : null
};
_tvvm.SubFields.Add(newProp);
}
}
}
}
为了设置 Tuple
属性值,我们添加一个字段来保存 Tuple
的值,并在值更改时设置属性输入字段的值。
public class TuplePropertyOutputValueViewModel : OutputValueViewModel
{
public TuplePropertyOutputValueViewModel(string name, object value, int valueLevel = 0)
: base(value, valueLevel)
{
Name = name;
}
}
#region TupleValue
public object TupleValue
{
get { return (object)GetValue(TupleValueProperty); }
set { SetValue(TupleValueProperty, value); }
}
public static readonly DependencyProperty TupleValueProperty =
DependencyProperty.Register("TupleValue", typeof(object),
typeof(TupleContentControl), new PropertyMetadata(null,
new PropertyChangedCallback(onTupleValueChanged)));
private static void onTupleValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs arg)
{
TupleContentControl tcc = o as TupleContentControl;
if (tcc != null)
{
tcc.setTupleValue();
}
}
#endregion
void setTupleValue()
{
if (TupleValue != null &&
TupleValue.GetType() == TupleType && IsTupleType(TupleType))
{
PropertyInfo[] tupleProps = TupleType.GetProperties();
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm == null || vvm.IsEditable)
{
if (_tvvm.SubFields.Count == tupleProps.Length)
{
int propInx = 0;
foreach (var prop in _tvvm.SubFields)
{
prop.Value = tupleProps[propInx].GetValue(TupleValue);
propInx++;
}
}
}
else
{
_tvvm.IsExpandedByDefault = true;
_tvvm.SubFields.Clear();
foreach (PropertyInfo prop in tupleProps)
{
_tvvm.SubFields.Add(
new TuplePropertyOutputValueViewModel(prop.Name, prop.GetValue(TupleValue))
{
DataTemplates = vvm.DataTemplates,
AutoGenerateCompatibleTypes = vvm.AutoGenerateCompatibleTypes,
KnownTypes = vvm.KnownTypes
});
}
}
}
}
在 setTupleValue
方法中,我们区分输入数据和输出数据两种情况。对于输入数据,我们只需为每个属性输入字段(已在 createTupleProperties
中创建)设置适当的属性值。对于输出数据,我们创建一个 OutputValueViewModel
,并用属性名和值对其进行设置。
为了获取 Tuple
的值,我们注册到每个属性输入字段的 PropertyChanged
事件,并在每个属性更改时创建一个 Tuple
对象实例(使用带有属性值的构造函数)。
private void createTupleProperties()
{
if (TupleType != null)
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm == null || vvm.IsEditable)
{
foreach (ValueViewModel oldProp in _tvvm.SubFields)
{
oldProp.PropertyChanged -= onTuplePropertyChanged;
}
_tvvm.SubFields.Clear();
foreach (var prop in TupleType.GetProperties())
{
var newProp = new PropertyInputValueViewModel(prop)
{
DataTemplates = vvm != null ? vvm.DataTemplates : null,
AutoGenerateCompatibleTypes = vvm != null ? vvm.AutoGenerateCompatibleTypes : false,
KnownTypes = vvm != null ? vvm.KnownTypes : null
};
_tvvm.SubFields.Add(newProp);
newProp.PropertyChanged += onTuplePropertyChanged;
}
}
}
}
private void onTuplePropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
{
updateTupleValue();
}
}
private void updateTupleValue()
{
if (IsTupleType(TupleType) && _tvvm.HasSubFields)
{
object[] ctorParams = _tvvm.SubFields.Select(p => p.Value).ToArray();
TupleValue = Activator.CreateInstance(TupleType, ctorParams);
}
}
查找 Tuple 类型
在拥有一个用于显示 Tuple
的控件后,我们可以将其设置为所需 Tuple
类型的数据模板(由于它是一个泛型类,可能会有无限数量的生成类型)。在创建数据模板之前,我们必须知道使用了哪些 Tuple 类型。这可以通过以下方式完成。
private static IEnumerable<Type> getTupleTypes(Type objectType, List<Type> usedTypes = null)
{
bool isRootType = false;
if (usedTypes == null)
{
usedTypes = new List<Type>();
isRootType = true;
}
if (usedTypes.Contains(objectType))
{
return _emptyTypes;
}
usedTypes.Add(objectType);
IEnumerable<Type> tupleTypes = objectType.GetProperties().Select(p => p.PropertyType);
tupleTypes = tupleTypes.Concat(objectType.GetFields().Select(f => f.FieldType));
tupleTypes = tupleTypes.Concat(objectType.GetMethods().SelectMany
(m => m.GetParameters().Select(p => p.ParameterType)));
tupleTypes = tupleTypes.Concat(objectType.GetMethods().Select(m => m.ReturnType));
IEnumerable<Type> subTypes = tupleTypes.SelectMany(t => getTupleTypes(t, usedTypes));
tupleTypes = tupleTypes.Concat(subTypes);
if (isRootType && IsTupleType(objectType))
{
tupleTypes.Concat(new Type[1] { objectType });
}
tupleTypes = tupleTypes.Where(t => IsTupleType(t)).Distinct();
return tupleTypes;
}
public static bool IsTupleType(Type t)
{
return t != null && t.IsGenericType && t.Name.StartsWith("Tuple`");
}
在 getTupleTypes
方法中,我们获取一个 Type
,遍历其所有属性、字段、方法的参数和返回值,并获取 Tuple
类型家族中使用的类型。我们还对内部类型递归地执行相同的过程,以涵盖它们的 Tuple
类型。
创建 Tuple 数据模板
使用我们的控件和获取到的 Tuple
类型,我们可以创建我们的数据模板扩展。这可以通过以下方式完成。
public static TypeDataTemplate[] GetTupleTypeDataTemplates(Type objectType)
{
if (objectType == null)
{
return null;
}
IEnumerable<Type> tupleTypes = getTupleTypes(objectType);
TypeDataTemplate[] res = tupleTypes.Select(t =>
{
FrameworkElementFactory controlFactory =
new FrameworkElementFactory(typeof(TupleContentControl));
controlFactory.SetBinding(TupleContentControl.TupleTypeProperty, new Binding
{
Source = t
});
controlFactory.SetBinding(TupleContentControl.TupleValueProperty, new Binding
{
Path = new PropertyPath("Value"),
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
return new TypeDataTemplate
{
ValueType = t,
ValueViewModelDataTemplate = new DataTemplate
{
VisualTree = controlFactory
}
};
}).ToArray();
return res;
}
在 GetTupleTypeDataTemplates
方法中,对于每个 Tuple
类型,我们创建一个带有我们的 TupleContentControl
控件的 FrameworkElementFactory
实例。在该实例上,我们将控件的 TupleType
属性绑定到 Tuple
的类型,并将控件的 TupleValue
属性绑定到 DataContext
(即模板化的 ValueViewModel
)的 Value
属性。使用创建的 FrameworkElementFactory
,我们创建一个 DataTemplate
,然后创建一个带有创建的数据模板和 Tuple
类型的 TypeDataTemplate
对象。
为了从 XAML 代码中使用此实用程序,我们添加了一个适当的转换器。
public class TupleTypesDataTemplatesFromTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Utils.GetTupleTypeDataTemplates(value as Type);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
显示固定对象属性
下一个挑战是显示对象的属性,而不能折叠它们或将对象的值设置为 null
。与 Tuple
情况一样,为了实现此行为,我们使用一个辅助控件。
public class FixedPropertiesInputItemsControl : ItemsControl
{
}
在该控件中,为了移除原始数据模板的行为(允许折叠和设置为 null
),我们将原始 SubFields
存储在一个单独的字段中,并清除原始 ValueViewModel
中的数据。
public class FixedPropertiesInputValueViewModel : InputValueViewModel
{
}
public class FixedPropertiesInputItemsControl : ItemsControl
{
private FixedPropertiesInputValueViewModel _fpivvm;
public FixedPropertiesInputItemsControl()
{
_fpivvm = new FixedPropertiesInputValueViewModel();
Initialized += onInitialized;
}
private void onInitialized(object sender, EventArgs e)
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm != null)
{
copyValueViewModel(vvm);
clearValueViewModel(vvm);
SetBinding(ItemsControl.ItemsSourceProperty,
new Binding { Source = _fpivvm, Path = new PropertyPath("SubFields") });
}
}
private void copyValueViewModel(ValueViewModel vvm)
{
_fpivvm.ValueType = vvm.ValueType;
_fpivvm.SelectedCompatibleType = vvm.SelectedCompatibleType;
_fpivvm.SubFields.Clear();
foreach (var subField in vvm.SubFields)
{
_fpivvm.SubFields.Add(subField);
}
}
private void clearValueViewModel(ValueViewModel vvm)
{
vvm.SubFields.Clear();
Type vvmType = typeof(ValueViewModel);
FieldInfo fi = vvmType.GetField("_selectedCompatibleType",
BindingFlags.NonPublic | BindingFlags.Instance);
fi?.SetValue(vvm, null);
MethodInfo mi = vvmType.GetMethod("NotifyPropertyChanged",
BindingFlags.NonPublic | BindingFlags.Instance);
mi?.Invoke(vvm, new object[] { "IsNullable" });
}
}
在 onInitialized
方法中,我们将原始 SubFields
复制到一个单独的字段,并将复制的 SubFields
设置为控件的 ItemsSource
属性。
在 clearValueViewModel
方法中,我们清除原始 SubFields
,将 private
的 _selectedCompatibleType
字段设置为 null
(此字段会影响 IsNullable
属性的值,该值决定是否显示“设为 null
”按钮),并通知 IsNullable
属性已更改。此实现有点棘手。它使用了 private
数据并假定了内部实现。但是,由于我认为我不会更改该实现,因此可以接受。
为了获取对象的值,我们注册到每个子字段的 PropertyChanged
事件,并在每个属性更改时,将原始 ValueViewModel
的值设置为新值。
public class FixedPropertiesInputValueViewModel : InputValueViewModel
{
protected override object GetValue()
{
if (HasSubFields)
{
return GenerateValueFromSubFields();
}
return _value;
}
}
private void copyValueViewModel(ValueViewModel vvm)
{
_fpivvm.ValueType = vvm.ValueType;
_fpivvm.SelectedCompatibleType = vvm.SelectedCompatibleType;
_fpivvm.SubFields.Clear();
foreach (var subField in vvm.SubFields)
{
subField.PropertyChanged += onSubPropertyChanged;
_fpivvm.SubFields.Add(subField);
}
}
private void onSubPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
{
ValueViewModel vvm = DataContext as ValueViewModel;
if (vvm != null)
{
vvm.Value = _fpivvm.Value;
clearValueViewModel(vvm);
}
}
}
我们使用派生 InputValueViewModel
的 GenerateValueFromSubFields
方法,从子字段构建新值。
使用辅助控件,我们创建一个 TypeDataTemplate
扩展(与我们为 Tuple
所做的方式相同)。
public static TypeDataTemplate GetFixedPropertiesInputTypeDataTemplate(Type objectType)
{
if (objectType == null)
{
return null;
}
return new TypeDataTemplate
{
ValueType = objectType,
ValueViewModelDataTemplate = new DataTemplate
{
VisualTree = new FrameworkElementFactory(typeof(FixedPropertiesInputItemsControl))
}
};
}
为了从 XAML 代码中使用此实用程序,我们添加了一个适当的转换器。
public class FixedPropertiesInputTypeDataTemplateFromTypeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return Utils.GetFixedPropertiesInputTypeDataTemplate(value as Type);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
为了使用一些 TypeDataTemplate
扩展,我们添加了一个用于合并 TypeDataTemplate
集合的转换器。
public class TypeDataTemplateCollectionConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return mergeValues(values).ToArray();
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
private IEnumerable<TypeDataTemplate> mergeValues(object[] values)
{
if (values != null)
{
foreach (object value in values)
{
if (value is TypeDataTemplate)
{
yield return value as TypeDataTemplate;
}
else if (value is IEnumerable<TypeDataTemplate>)
{
IEnumerable<TypeDataTemplate> collection = value as IEnumerable<TypeDataTemplate>;
foreach (TypeDataTemplate elem in collection)
{
yield return elem;
}
}
}
}
yield break;
}
}
如何使用
使用 Tuple 扩展
为了演示 Tuple
扩展的用法,我们添加了一个包含 2 个使用 Tuple
类型的方法的接口。
public enum OperationOperator
{
ADD,
SUB,
MUL,
DIV
}
public interface IMyOperations
{
Tuple<int, int> Div(Tuple<int, int> numbers);
Tuple<double, double, double[]> DoOperations(Tuple<Tuple<double, double, OperationOperator>,
Tuple<double, double, OperationOperator>, Tuple<double, double, OperationOperator>[]> operations);
}
以及一个实现该接口的类。
public class MyOperations : IMyOperations
{
public Tuple<int, int> Div(Tuple<int, int> numbers)
{
int result = numbers.Item1 / numbers.Item2;
int reminder = numbers.Item1 % numbers.Item2;
return new Tuple<int, int>(result, reminder);
}
public Tuple<double, double, double[]> DoOperations
(Tuple<Tuple<double, double, OperationOperator>, Tuple<double, double, OperationOperator>,
Tuple<double, double, OperationOperator>[]> operations)
{
double[] arrayRes = null;
if (operations.Item3 != null)
{
arrayRes = new double[operations.Item3.Length];
for (int operInx = 0; operInx < operations.Item3.Length; operInx++)
{
arrayRes[operInx] = doOperation(operations.Item3[operInx]);
}
}
return new Tuple<double, double, double[]>(
doOperation(operations.Item1), doOperation(operations.Item2), arrayRes);
}
private double doOperation(Tuple<double, double, OperationOperator> operation)
{
if (operation == null)
{
return 0;
}
switch (operation.Item3)
{
case OperationOperator.ADD:
return operation.Item1 + operation.Item2;
case OperationOperator.SUB:
return operation.Item1 - operation.Item2;
case OperationOperator.MUL:
return operation.Item1 * operation.Item2;
case OperationOperator.DIV:
return operation.Item1 / operation.Item2;
}
return 0;
}
}
在 Div
方法中,我们获取一个包含两个操作数的 Tuple
参数,并返回一个包含除法结果和余数的 Tuple
。
在 DoOperations
方法中,我们获取一个包含内部 Tuple
属性和一个 Tuple
集合的 Tuple
参数。每个内部 Tuple
包含操作数和运算符。返回值是一个包含操作结果的 Tuple
。
为了显示操作 GUI,我们添加了一个包含操作对象和接口类型的属性的视图模型。
public class BaseViewModel : INotifyPropertyChanged
{
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propName));
}
}
#endregion
}
public class ExampleViewModel : BaseViewModel
{
public ExampleViewModel()
{
OperationsObject = new MyOperations();
OperationsInterfaceType = typeof(IMyOperations);
}
public MyOperations OperationsObject { get; set; }
public Type OperationsInterfaceType { get; set; }
}
以及一个显示这些属性的 InterfacePresenter
控件。
<objectPresentation:InterfacePresenter ObjectInstance = "{Binding OperationsObject}"
InterfaceType="{Binding OperationsInterfaceType}" />
为了使 Tuple
类型能够工作,我们使用 Tuple
扩展来添加(对于所呈现对象中每个 Tuple
类型所需的)Tuple
数据模板。
<Window.Resources>
<objectPresentationConverters:TupleTypesDataTemplatesFromTypeConverter
x:Key="TupleTypesDataTemplatesFromTypeConverter" />
</Window.Resources>
...
<objectPresentation:InterfacePresenter ObjectInstance = "{Binding OperationsObject}"
InterfaceType="{Binding OperationsInterfaceType}"
DataTemplates="{Binding OperationsInterfaceType,
Converter={StaticResource TupleTypesDataTemplatesFromTypeConverter}}" />
结果是:
使用固定属性输入扩展
为了演示固定属性输入扩展的用法,我们使用与原始 ValuePresenter 示例 相同的示例,并将其改进为作为固定属性视图进行呈现。
为了显示固定属性视图,我们为形状类型(除了现有的 Color
数据模板之外)添加了“固定属性”数据模板。
<objectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}">
<objectPresentation:ValuePresenter.DataTemplates>
<MultiBinding Converter = "{StaticResource TypeDataTemplateCollectionConverter}" >
<Binding Source="{StaticResource typeDataTemplates}" />
<Binding Source = "{x:Type local:MyShape}"
Converter="{StaticResource FixedPropertiesInputTypeDataTemplateFromTypeConverter}" />
<Binding Source = "{x:Type local:MyRectangle}"
Converter="{StaticResource FixedPropertiesInputTypeDataTemplateFromTypeConverter}" />
<Binding Source = "{x:Type local:MyCircle}"
Converter="{StaticResource FixedPropertiesInputTypeDataTemplateFromTypeConverter}" />
</MultiBinding>
</objectPresentation:ValuePresenter.DataTemplates>
</objectPresentation:ValuePresenter>
我们还添加了一个默认形状值作为初始呈现的形状。
public class ExampleViewModel : BaseViewModel
{
// ...
public MyShape InitialShape { get; set; }
}
<objectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}"
Value="{Binding InitialShape, Mode=OneTime}">
...
</objectPresentation:ValuePresenter>
结果是:
结论
在本文中,我们创建了两个 TypeDataTample
扩展,并使用它们来改进 ObjectPresentation
库控件的行为。
在第一个扩展中,我们创建了一个解决方案来显示使用 Tuple
类型的组件。为此,我们创建了一个实用程序,用于为给定类型中使用的每个 Tuple
类型创建可以显示 Tuple
类型的数据模板。
在第二个扩展中,我们创建了一个解决方案来移除折叠属性视图或将对象设为 null
的能力。这可以用于在需要时显示固定属性视图。
目前,我们只有这两个扩展。但是,如果发现合适的需要,可能会向本文添加更多扩展。所以,如果您对有价值的扩展有什么想法,请随时在下方留言。