ObjectPresenter - 如何从给定对象生成对象的测试 GUI






4.94/5 (27投票s)
在本文中,我将一步一步地解释如何创建一个WPF自定义控件,该控件接收一个对象,并生成一个GUI,用于编辑该对象的属性和调用该对象的方法。
目录
引言
当我们想测试我们的.NET应用程序时,除了整个应用程序测试外,有时我们还想提供一种单独测试某些算法的方法。
通常,为了达到这个目标,我过去会创建一些GUI屏幕,用于输入所需的参数,并通过这些参数调用所需的方法。
这样一来,我经常发现自己浪费了大量时间来开发不属于开发应用程序一部分的GUI屏幕。除了屏幕的首次开发外,每次方法签名发生更改时,我都需要更改这些GUI屏幕。这样,我需要测试的每个模块都会给我一个需要维护的屏幕。
由于我厌倦了每次需要测试一个类时都这样做,所以我决定编写一个控件来简化这项任务。
背景
在此解决方案中,我们创建了一个WPF控件,该控件可以为我们想要测试的每个类呈现所需的GUI,并且会根据该类的更改进行更新。
为了呈现值,我们创建了视图模型和适当的DataTemplate
来呈现它们。
有关更多信息,您可以阅读MSDN关于反射、控件创作、路由事件、命令以及MVVM模式的主题。
工作原理
处理值
值视图模型
在创建允许调用方法的控件之前,我们需要一种方法来获取方法的参数值。为此,我们创建了一个视图模型来呈现值的详细信息。
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
}
class ValueViewModel : BaseViewModel
{
}
为值的名称、值的类型和值本身添加属性。
#region Name
private string _name;
public string Name
{
get { return _name; }
protected set
{
if (_name != value)
{
_name = value;
NotifyPropertyChanged("Name");
NotifyPropertyChanged("HasName");
}
}
}
#endregion
#region HasName
public bool HasName
{
get { return !string.IsNullOrEmpty(Name); }
}
#endregion
#region ValueType
private Type _valueType;
public Type ValueType
{
get { return _valueType; }
set
{
if (_valueType != value)
{
Type oldValue = _valueType;
_valueType = value;
OnValueTypeChanged(oldValue, _valueType);
}
}
}
protected virtual void OnValueTypeChanged(Type oldValue, Type newValue)
{
NotifyPropertyChanged("ValueType");
}
#endregion
#region Value
protected object _value;
public virtual object Value
{
get { return _value; }
set
{
if (_value != value)
{
object oldValue = _value;
_value = value;
OnValueChanged(oldValue, _value);
}
}
}
protected virtual void OnValueChanged(object oldValue, object newValue)
{
NotifyPropertyChanged("Value");
}
#endregion
此外,添加属性以指示值的种类和内容。
#region IsCollection
public bool IsCollection
{
get
{
Type t = SelectedCompatibleType;
if (t == null)
{
return false;
}
if (t == typeof(string))
{
return false;
}
Type[] typeIntefaces = t.GetInterfaces();
if (typeIntefaces.Contains(typeof(IEnumerable)))
{
return true;
}
return false;
}
}
#endregion
#region IsString
public bool IsString
{
get { return SelectedCompatibleType == typeof(string); }
}
#endregion
#region IsParsable
public bool IsParsable
{
get
{
Type t = SelectedCompatibleType;
if (t == null)
{
return false;
}
MethodInfo parseMethod = t.GetMethod("Parse", new[] { typeof(string) });
if (parseMethod != null && parseMethod.IsStatic)
{
return true;
}
return false;
}
}
#endregion
#region IsEnum
public bool IsEnum
{
get
{
Type t = SelectedCompatibleType;
if (t == null)
{
return false;
}
return t.IsEnum;
}
}
#endregion
#region IsBoolean
public bool IsBoolean
{
get { return SelectedCompatibleType == typeof(bool); }
}
#endregion
处理派生类型
为了支持获取派生自参数值类型的类型的参数值,我们需要找到所有与参数类型兼容的类型。这可以通过以下方式完成:
#region KnownTypes
private IEnumerable<Type> _knownTypes;
public IEnumerable<Type> KnownTypes
{
get { return _knownTypes; }
set
{
if (_knownTypes != value)
{
IEnumerable<Type> oldValue = _knownTypes;
_knownTypes = value;
OnKnownTypesChanged(oldValue, _knownTypes);
}
}
}
protected virtual void OnKnownTypesChanged
(IEnumerable<Type> oldValue, IEnumerable<Type> newValue)
{
NotifyPropertyChanged("KnownTypes");
}
#endregion
#region AutoGenerateCompatibleTypes
private bool _autoGenerateCompatibleTypes;
public bool AutoGenerateCompatibleTypes
{
get { return _autoGenerateCompatibleTypes; }
set
{
if (_autoGenerateCompatibleTypes != value)
{
bool oldValue = _autoGenerateCompatibleTypes;
_autoGenerateCompatibleTypes = value;
OnAutoGenerateCompatibleTypesChanged(oldValue, _autoGenerateCompatibleTypes);
}
}
}
protected virtual void OnAutoGenerateCompatibleTypesChanged(bool oldValue, bool newValue)
{
NotifyPropertyChanged("AutoGenerateCompatibleTypes");
}
#endregion
private List<Type> GetCompatibleTypes(Type baseType)
{
List<Type> res = new List<Type>();
if (!baseType.IsAbstract)
{
res.Add(baseType);
}
AddKnownCompatibleTypes(baseType, res);
if (AutoGenerateCompatibleTypes &&
baseType != typeof(object))
{
AddCompatibleTypesFromLoadedAssemblies(baseType, res);
}
return res.Distinct().ToList();
}
private void AddKnownCompatibleTypes(Type baseType, List<Type> res)
{
if (KnownTypes != null)
{
foreach (Type t in KnownTypes)
{
if (t.IsSubclassOf(baseType) && !t.IsAbstract)
{
res.Add(t);
}
}
}
}
private static void AddCompatibleTypesFromLoadedAssemblies(Type baseType, List<Type> res)
{
Assembly[] assemblies = GetLoadedAssemblies();
if (assemblies == null)
{
return;
}
foreach (Assembly a in assemblies)
{
if (a == null)
{
continue;
}
foreach (Type t in a.GetTypes())
{
if (t.IsSubclassOf(baseType) && !t.IsAbstract)
{
res.Add(t);
}
}
}
}
static private Assembly[] GetLoadedAssemblies()
{
AppDomain currDomain = AppDomain.CurrentDomain;
if (currDomain == null)
{
return null;
}
return currDomain.GetAssemblies();
}
GetCompatibleTypes
方法生成一个列表,其中包含参数的类型(如果它不是抽象的),以及在已加载的程序集中派生自参数类型的类型。还有一个选项是通过KnownTypes
和AutoGenerateCompatibleTypes
属性仅从给定的类型列表中生成类型。
我们生成兼容的类型,并设置默认选定的兼容类型,以便在ValueType
更改的每次都如此:
#region CompatibleTypes
private ObservableCollection<Type> _compatibleTypes;
public ObservableCollection<Type> CompatibleTypes
{
get { return _compatibleTypes ?? (_compatibleTypes = new ObservableCollection<Type>()); }
}
#endregion
#region SelectedCompatibleType
private Type _selectedCompatibleType;
public Type SelectedCompatibleType
{
get { return _selectedCompatibleType; }
set
{
if (_selectedCompatibleType != value)
{
Type oldValue = _selectedCompatibleType;
_selectedCompatibleType = value;
OnSelectedCompatibleTypeChanged(oldValue, _selectedCompatibleType);
}
}
}
protected virtual void OnSelectedCompatibleTypeChanged(Type oldValue, Type newValue)
{
NotifyPropertyChanged("IsString");
NotifyPropertyChanged("IsParsable");
NotifyPropertyChanged("IsCollection");
NotifyPropertyChanged("IsEnum");
NotifyPropertyChanged("IsBoolean");
NotifyPropertyChanged("Value");
}
#endregion
protected virtual void OnValueTypeChanged(Type oldValue, Type newValue)
{
UpdateCompatibleTypes();
NotifyPropertyChanged("ValueType");
}
protected void UpdateCompatibleTypes()
{
CompatibleTypes.Clear();
if (ValueType == null)
{
return;
}
GetCompatibleTypes(ValueType).ForEach(t => CompatibleTypes.Add(t));
SelectedCompatibleType = CompatibleTypes.FirstOrDefault();
}
处理复杂类型
为了支持获取复杂类型的值,我们需要呈现该类型的属性和字段的值。这可以通过以下方式完成:
#region SubFields
private ObservableCollection<ValueViewModel> _subFields;
public ObservableCollection<ValueViewModel> SubFields
{
get { return _subFields ?? (_subFields = new ObservableCollection<ValueViewModel>()); }
}
#endregion
#region HasSubFields
public bool HasSubFields
{
get { return SubFields.Count != 0; }
}
#endregion
protected virtual void GenerateSubFieldsIfNeeded()
{
SubFields.Clear();
Type t = SelectedCompatibleType;
if (t == null)
{
return;
}
if (IsString ||
IsParsable ||
IsCollection ||
IsEnum)
{
return;
}
PropertyInfo[] typeProperties = t.GetProperties();
foreach (PropertyInfo pi in typeProperties)
{
if (pi.CanWrite)
{
AddPropertyValueViewModel(pi);
}
}
FieldInfo[] typeFields = t.GetFields();
foreach (FieldInfo fi in typeFields)
{
if (fi.IsPublic)
{
AddFieldValueViewModel(fi);
}
}
}
protected abstract void AddPropertyValueViewModel(PropertyInfo pi);
protected abstract void AddFieldValueViewModel(FieldInfo fi);
GenerateSubFieldsIfNeeded
方法对类型的属性调用AddPropertyValueViewModel
abstract
方法,并对类型的字段调用AddFieldValueViewModel
abstract
方法。这些方法在派生类中得到适当的实现。
为了获取值,我们派生ValueViewModel
如下:
public class InputValueViewModel : ValueViewModel
{
public InputValueViewModel()
{
IsEditable = true;
}
protected override void AddPropertyValueViewModel(PropertyInfo pi)
{
PropertyInputValueViewModel subField = new PropertyInputValueViewModel(pi)
{
KnownTypes = KnownTypes,
AutoGenerateCompatibleTypes = AutoGenerateCompatibleTypes
};
SubFields.Add(subField);
}
protected override void AddFieldValueViewModel(FieldInfo fi)
{
FieldInputValueViewModel subField = new FieldInputValueViewModel(fi)
{
KnownTypes = KnownTypes,
AutoGenerateCompatibleTypes = AutoGenerateCompatibleTypes
};
SubFields.Add(subField);
}
}
public class PropertyInputValueViewModel : InputValueViewModel
{
public PropertyInputValueViewModel(PropertyInfo pi)
{
PropertyInformation = pi;
}
#region PropertyInformation
private PropertyInfo _propertyInformation;
public PropertyInfo PropertyInformation
{
protected get { return _propertyInformation; }
set
{
if (_propertyInformation != value)
{
PropertyInfo oldValue = _propertyInformation;
_propertyInformation = value;
OnPropertyInformationChanged(oldValue, _propertyInformation);
}
}
}
protected virtual void OnPropertyInformationChanged
(PropertyInfo oldValue, PropertyInfo newValue)
{
if (newValue == null)
{
return;
}
Name = newValue.Name;
ValueType = newValue.PropertyType;
}
#endregion
}
public class FieldInputValueViewModel : InputValueViewModel
{
public FieldInputValueViewModel(FieldInfo fi)
{
FieldInformation = fi;
}
#region FieldInformation
private FieldInfo _fieldInformation;
public FieldInfo FieldInformation
{
protected get { return _fieldInformation; }
set
{
if (_fieldInformation != value)
{
FieldInfo oldValue = _fieldInformation;
_fieldInformation = value;
OnFieldInformationChanged(oldValue, _fieldInformation);
}
}
}
protected virtual void OnFieldInformationChanged
(FieldInfo oldValue, FieldInfo newValue)
{
if (newValue == null)
{
return;
}
Name = newValue.Name;
ValueType = newValue.FieldType;
}
#endregion
}
IsEditable
属性在ValueViewModel
中定义如下:
#region IsEditable
private bool _isEditable;
public bool IsEditable
{
get { return _isEditable; }
protected set
{
if (_isEditable != value)
{
_isEditable = value;
NotifyPropertyChanged("IsEditable");
}
}
}
#endregion
为了呈现值,我们派生ValueViewModel
如下:
public class OutputValueViewModel : ValueViewModel
{
#region Constructors
public OutputValueViewModel(object value)
{
IsEditable = false;
if (value != null)
{
Type valueType = value.GetType();
if (valueType.IsEnum)
{
// Set the enum as string.
Value = Enum.GetName(valueType, value);
ValueType = typeof(string);
}
else if (value is Exception)
{
Value = new ExceptionData(value as Exception);
ValueType = typeof(ExceptionData);
}
else
{
Value = value;
ValueType = value.GetType();
}
}
}
#endregion
protected override void AddFieldValueViewModel(FieldInfo fi)
{
if (fi == null || Value == null)
{
return;
}
object fieldValue = fi.GetValue(Value);
SubFields.Add(new OutputValueViewModel(fieldValue)
{
Name = fi.Name
});
}
protected override void AddPropertyValueViewModel(PropertyInfo pi)
{
if (pi == null || Value == null)
{
return;
}
object prorertyValue = pi.GetValue(Value, null);
SubFields.Add(new OutputValueViewModel(prorertyValue)
{
Name = pi.Name
});
}
}
OutputValueViewModel
类使用ExceptionData
类来呈现异常。该类实现如下:
public class ExceptionData
{
public ExceptionData(Exception ex)
{
if (ex != null)
{
ExceptionType = ex.GetType().FullName;
ExceptionMessage = ex.Message;
if (ex.InnerException != null)
{
InnerException = new ExceptionData(ex.InnerException);
}
}
}
public string ExceptionType { get; set; }
public string ExceptionMessage { get; set; }
public ExceptionData InnerException { get; set; }
}
处理集合
为了支持获取集合,我们必须允许用户添加和删除集合的元素。我们可以通过添加用于添加和删除集合元素的命令来做到这一点。
#region CollectionElements
private ObservableCollection<ValueViewModel> _collectionElements;
public ObservableCollection<ValueViewModel> CollectionElements
{
get { return _collectionElements ??
(_collectionElements = new ObservableCollection<ValueViewModel>()); }
}
#endregion
#region IsRemovable
private bool _isRemovable;
public bool IsRemovable
{
get { return _isRemovable; }
protected set
{
if (_isRemovable != value)
{
_isRemovable = value;
NotifyPropertyChanged("IsRemovable");
}
}
}
#endregion
#region AddNewCollectionElementCommand
private ICommand _addNewCollectionElementCommand;
public ICommand AddNewCollectionElementCommand
{
get
{
if (_addNewCollectionElementCommand == null)
{
_addNewCollectionElementCommand =
new GeneralCommand(o => AddNewCollectionElement(true), o => IsCollection);
}
return _addNewCollectionElementCommand;
}
}
protected void AddNewCollectionElement(bool notifyValueChanged)
{
InputValueViewModel element = new InputValueViewModel
{
ValueType = CollectionElementType,
IsRemovable = true,
Removed = OnCollectionElementRemoved,
KnownTypes = KnownTypes,
AutoGenerateCompatibleTypes = AutoGenerateCompatibleTypes
};
CollectionElements.Add(element);
if (notifyValueChanged)
{
NotifyPropertyChanged("Value");
}
}
protected void OnCollectionElementRemoved(ValueViewModel element)
{
CollectionElements.Remove(element);
}
#endregion
#region RemoveCommand
private ICommand _removeCommand;
public ICommand RemoveCommand
{
get { return _removeCommand ??
(_removeCommand = new GeneralCommand(o => Remove(), o => IsRemovable)); }
}
protected void Remove()
{
Action<ValueViewModel> handler = Removed;
if (handler != null)
{
handler(this);
}
}
public Action<ValueViewModel> Removed;
#endregion
为了创建命令,我们使用GeneralCommand
类。该类实现如下:
public class GeneralCommand : ICommand
{
private Action<object> _execute;
private Predicate<object> _canExecute;
public GeneralCommand(Action<object> execute, Predicate<object> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
if (_canExecute != null)
{
return _canExecute(parameter);
}
return true;
}
public event EventHandler CanExecuteChanged
{
add
{
CommandManager.RequerySuggested += value;
}
remove
{
CommandManager.RequerySuggested -= value;
}
}
public void Execute(object parameter)
{
if (_execute != null)
{
_execute(parameter);
}
}
#endregion
}
为了呈现集合,我们在OutputValueViewModel
中重写GenerateSubFieldsIfNeeded
方法如下:
protected override void GenerateSubFieldsIfNeeded()
{
base.GenerateSubFieldsIfNeeded();
if (IsCollection)
{
IEnumerable ie = Value as IEnumerable;
if (ie != null)
{
foreach (object val in ie)
{
CollectionElements.Add(new OutputValueViewModel(val));
}
}
}
}
处理Null值
为了支持获取null
值,我们添加属性来指示该值为null
。
#region IsNullable
public bool IsNullable
{
get
{
Type t = SelectedCompatibleType;
return t != null ? !t.IsValueType : false;
}
}
#endregion
#region IsNull
protected bool _isNull;
public bool IsNull
{
get { return _isNull; }
set
{
if (_isNull != value)
{
bool oldValue = _isNull;
_isNull = value;
OnIsNullChanged(oldValue, _isNull);
NotifyPropertyChanged("IsNull");
}
}
}
protected virtual void OnIsNullChanged(bool oldValue, bool newValue)
{
}
#endregion
值数据模板
我们有了视图模型之后,就创建一个DataTemplate
来呈现这个视图模型。
<DataTemplate DataType="{x:Type local:ValueViewModel}">
</DataTemplate>
在这个DataTemplate
中,我们放置一个Grid
,并将其分成3列。
<DataTemplate DataType="{x:Type local:ValueViewModel}">
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
<Grid.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Grid.LayoutTransform>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
</Grid>
</DataTemplate>
在这个Grid
中,我们添加一个“删除”按钮。
<DataTemplate DataType="{x:Type local:ValueViewModel}">
<DataTemplate.Resources>
<Storyboard x:Key="hideRootElementStoryboard">
<DoubleAnimation Storyboard.TargetName="rootElement"
Storyboard.TargetProperty="(FrameworkElement.
LayoutTransform).(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="rootElement"
Storyboard.TargetProperty="(FrameworkElement.
LayoutTransform).(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</DataTemplate.Resources>
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
...
<local:SuspendedButton x:Name="btnRemove"
Style="{StaticResource removeButtonStyle}"
ToolTip="Remove element."
SuspendTime="0:0:0.2"
Margin="1"
Command="{Binding RemoveCommand}"
Visibility="Collapsed"
VerticalAlignment="Top"
HorizontalAlignment="Left" />
</Grid>
<DataTemplate.Triggers>
<EventTrigger RoutedEvent="local:SuspendedButton.BeforeClick"
SourceName="btnRemove">
<BeginStoryboard Storyboard=
"{StaticResource hideRootElementStoryboard}" />
</EventTrigger>
</DataTemplate.Triggers>
</DataTemplate>
添加一个用于字段名称和值的区域。
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
...
<Grid x:Name="fieldRegion"
Grid.Column="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal"
Visibility="{Binding HasName,
Converter={StaticResource BooleanToVisibilityConverter}}">
<TextBlock Text="{Binding Name}" />
<TextBlock Text=": " />
</StackPanel>
<Grid x:Name="isNullRegion"
Grid.Column="1"
Margin="2">
<ToggleButton Visibility="{Binding IsNullable,
Converter={StaticResource BooleanToVisibilityConverter}}"
Style="{StaticResource nullButtonStyle}"
VerticalAlignment="Top"
IsChecked="{Binding IsNull, Mode=TwoWay}" />
</Grid>
<ContentControl x:Name="fieldValue"
Content="{Binding}"
Grid.Column="2" />
</Grid>
</Grid>
并添加一个用于附加兼容类型的区域。
<DataTemplate DataType="{x:Type local:ValueViewModel}">
<DataTemplate.Resources>
...
<Storyboard x:Key="showTypesStoryboard">
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleX)"
To="1"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="hideTypesStoryboard">
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="typesBorder"
Storyboard.TargetProperty=
"(FrameworkElement.LayoutTransform).(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</DataTemplate.Resources>
<Grid x:Name="rootElement"
HorizontalAlignment="Stretch"
Margin="1">
...
<Grid Visibility="{Binding HasAdditionalCompatibleTypes,
Converter={StaticResource BooleanToVisibilityConverter}}"
Grid.Column="2"
VerticalAlignment="Top"
Margin="5,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ToggleButton x:Name="typesToggle"
Style="{StaticResource expandButtonStyle}"
HorizontalAlignment="Right"
ToolTip="Expand the compatible types region.">
<ToggleButton.LayoutTransform>
<ScaleTransform ScaleX="-1" />
</ToggleButton.LayoutTransform>
</ToggleButton>
<Border x:Name="typesBorder"
Grid.Row="1"
Margin="0,3"
BorderThickness="1"
CornerRadius="3"
BorderBrush="#CC000000"
Background="#44000000">
<Border.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Border.LayoutTransform>
<StackPanel Margin="5">
<TextBlock Text="Types"
HorizontalAlignment="Center"
FontSize="14"
Margin="0,0,0,5"
Foreground="#EEFFFFFF"/>
<ListBox ItemsSource="{Binding CompatibleTypes}"
SelectedItem="{Binding SelectedCompatibleType, Mode=TwoWay}"
Background="Transparent"
BorderBrush="Transparent"
MaxHeight="250">
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Grid>
<Border x:Name="selectedTypeBackground"
Background="#99FFFFFF"
BorderThickness="1"
BorderBrush="#DDFFFFFF"
CornerRadius="3"
Visibility="Hidden"/>
<ContentPresenter Margin="3" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected"
Value="True">
<Setter TargetName=
"selectedTypeBackground"
Property="Visibility"
Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</StackPanel>
</Border>
</Grid>
</Grid>
<DataTemplate.Triggers>
...
<Trigger SourceName="typesToggle"
Property="IsChecked"
Value="True">
<Setter TargetName="typesToggle"
Property="ToolTip"
Value="Collapse the compatible types region." />
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource showTypesStoryboard}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource hideTypesStoryboard}" />
</Trigger.ExitActions>
</Trigger>
</DataTemplate.Triggers>
</DataTemplate>
对于“删除”按钮,我们使用SuspendedButton,以便在字段被删除之前运行动画。
在兼容类型区域,我们有一个ListBox
用于选择值的类型。
在字段区域,我们有一个TextBlock
用于显示字段名称,一个ToggleButton
用于确定值是否为null
,以及一个ContentControl
用于显示字段的值。
为了以不同的方式呈现不同的类型,我们为这些类型创建了数据模板。
<DataTemplate x:Key="regularFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="booleanFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="enumFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="complexFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="collectionFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
<DataTemplate x:Key="nullFieldDataTemplate"
DataType="{x:Type local:ValueViewModel}">
...
</DataTemplate>
并相应地更改值ContentControl
的ContentTemplate
。
<DataTemplate DataType="{x:Type local:ValueViewModel}">
...
<DataTemplate.Triggers>
...
<DataTrigger Binding="{Binding IsString}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource regularFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsParsable}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource regularFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding HasSubFields}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource complexFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsCollection}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource collectionFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsEnum}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource enumFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsBoolean}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource booleanFieldDataTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsNull}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{StaticResource nullFieldDataTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
其他数据模板
为了支持特定类型的不同数据模板,我们必须为用户提供一种定义特定类型数据模板的方法。我们可以通过在ValueViewModel
中保存类型的DataTemplate
来做到这一点。
public class TypeDataTemplate : BaseViewModel
{
#region Properties
#region ValueType
private Type _valueType;
public Type ValueType
{
get { return _valueType; }
set
{
if (_valueType != value)
{
_valueType = value;
NotifyPropertyChanged("ValueType");
}
}
}
#endregion
#region ValueViewModelDataTemplate
private DataTemplate _valueViewModelDataTemplate;
public DataTemplate ValueViewModelDataTemplate
{
get { return _valueViewModelDataTemplate; }
set
{
if (_valueViewModelDataTemplate != value)
{
_valueViewModelDataTemplate = value;
NotifyPropertyChanged("ValueViewModelDataTemplate");
}
}
}
#endregion
#endregion
}
public abstract class ValueViewModel : BaseViewModel
{
...
#region DataTemplates
private IEnumerable<TypeDataTemplate> _dataTemplates;
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return _dataTemplates; }
set
{
if (_dataTemplates != value)
{
IEnumerable<TypeDataTemplate> oldValue = _dataTemplates;
_dataTemplates = value;
OnDataTemplatesChanged(oldValue, _dataTemplates);
NotifyPropertyChanged("DataTemplates");
}
}
}
protected virtual void OnDataTemplatesChanged
(IEnumerable<TypeDataTemplate> oldValue, IEnumerable<TypeDataTemplate> newValue)
{
}
#endregion
...
}
根据SelectedCompatibleType
设置相应的数据模板(如果存在)。
protected virtual void OnDataTemplatesChanged(IEnumerable<TypeDataTemplate> oldValue,
IEnumerable<TypeDataTemplate> newValue)
{
ValueDataTemplate = null;
if (newValue != null)
{
Type t = SelectedCompatibleType;
if (t != null)
{
ValueDataTemplate = newValue.Where(tdt1 => tdt1.ValueType == t).
Select(tdt2 => tdt2.ValueViewModelDataTemplate).FirstOrDefault();
}
foreach (ValueViewModel subField in SubFields)
{
subField.DataTemplates = newValue;
}
foreach (ValueViewModel elem in CollectionElements)
{
elem.DataTemplates = newValue;
}
}
}
#region ValueDataTemplate
private DataTemplate _valueDataTemplate;
public DataTemplate ValueDataTemplate
{
get { return _valueDataTemplate; }
set
{
if (_valueDataTemplate != value)
{
_valueDataTemplate = value;
NotifyPropertyChanged("ValueDataTemplate");
NotifyPropertyChanged("HasValueDataTemplate");
}
}
}
#endregion
#region HasValueDataTemplate
public bool HasValueDataTemplate
{
get { return ValueDataTemplate != null; }
}
#endregion
并添加一个DataTrigger
来设置相应的数据模板。
<DataTemplate DataType="{x:Type local:ValueViewModel}">
...
<DataTemplate.Triggers>
...
<DataTrigger Binding="{Binding HasValueDataTemplate}"
Value="True">
<Setter TargetName="fieldValue"
Property="ContentTemplate"
Value="{Binding ValueDataTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
按钮样式
除了数据模板之外,我们还为一些按钮添加了样式。
对于“删除”按钮,我们添加以下样式()
<Style x:Key="removeButtonStyle" TargetType="{x:Type Button}">
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="DarkRed" />
<Setter Property="Background"
Value="Red" />
<Setter Property="Foreground"
Value="White" />
<Setter Property="Width"
Value="15" />
<Setter Property="Height"
Value="15" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<ControlTemplate.Resources>
<Storyboard x:Key="mouseEnterStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="1"
Duration="0:0:0.05" />
</Storyboard>
<Storyboard x:Key="mouseLeaveStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="0.5"
Duration="0:0:0.15" />
</Storyboard>
</ControlTemplate.Resources>
<Grid x:Name="rootElement"
Opacity="0.5">
<Ellipse Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="1"
Fill="{TemplateBinding Background}" />
<Line X1="1" Y1="1" X2="7" Y2="7"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
<Line X1="1" Y1="7" X2="7" Y2="1"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseEnterStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseLeaveStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"
Value="True">
<Setter Property="Background"
Value="#FFCC0000" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
对于“展开”按钮,我们添加以下样式()
<Style x:Key="expandButtonStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="DarkBlue" />
<Setter Property="Background"
Value="LightBlue" />
<Setter Property="Foreground"
Value="Cyan" />
<Setter Property="Width"
Value="12" />
<Setter Property="Height"
Value="12" />
<Setter Property="Padding"
Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<ControlTemplate.Resources>
<Storyboard x:Key="mouseEnterStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="1"
Duration="0:0:0.05" />
</Storyboard>
<Storyboard x:Key="mouseLeaveStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="rootElement"
To="0.5"
Duration="0:0:0.15" />
</Storyboard>
<Storyboard x:Key="checkedStoryboard">
<DoubleAnimation Storyboard.TargetName="mainShape"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(RotateTransform.Angle)"
To="45"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="uncheckedStoryboard">
<DoubleAnimation Storyboard.TargetName="mainShape"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(RotateTransform.Angle)"
To="-45"
Duration="0:0:0.2" />
</Storyboard>
</ControlTemplate.Resources>
<Grid x:Name="rootElement"
Opacity="0.5">
<Polygon x:Name="mainShape"
Stroke="{TemplateBinding BorderBrush}"
StrokeThickness="2"
StrokeLineJoin="Round"
Fill="{TemplateBinding Background}"
RenderTransformOrigin="0.5,0.5"
Points="0,10 10,10 10,0">
<Polygon.RenderTransform>
<RotateTransform Angle="-45" />
</Polygon.RenderTransform>
</Polygon>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseEnterStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseLeaveStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"
Value="True">
<Setter Property="Background"
Value="#FF0000CC" />
</Trigger>
<Trigger Property="IsChecked"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource checkedStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource uncheckedStoryboard}" />
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
对于“Null”按钮,我们添加以下样式()
<Style x:Key="nullButtonStyle" TargetType="{x:Type ToggleButton}">
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="#CC000000" />
<Setter Property="Background"
Value="#44000000" />
<Setter Property="Foreground"
Value="White" />
<Setter Property="Width"
Value="20" />
<Setter Property="Height"
Value="20" />
<Setter Property="Padding"
Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<ControlTemplate.Resources>
<Storyboard x:Key="mouseEnterStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="backgroundBorder"
To="1"
Duration="0:0:0.05" />
</Storyboard>
<Storyboard x:Key="mouseLeaveStoryboard">
<DoubleAnimation Storyboard.TargetProperty="Opacity"
Storyboard.TargetName="backgroundBorder"
To="0.5"
Duration="0:0:0.15" />
</Storyboard>
</ControlTemplate.Resources>
<Grid >
<Border x:Name="backgroundBorder"
Opacity="0.5"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="2"/>
<Grid x:Name="nullSign"
Opacity="0.5">
<Ellipse Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
Fill="Transparent"
Margin="4" />
<Line X1="0" Y1="14" X2="14" Y2="0"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="2"
Margin="3" />
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseEnterStoryboard}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource mouseLeaveStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="IsPressed"
Value="True">
<Setter Property="Background"
Value="#AA000000" />
</Trigger>
<Trigger Property="IsChecked"
Value="True">
<Setter TargetName="nullSign"
Property="Opacity"
Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
从输入生成值
为了从值的整体部分(例如,集合元素、类属性等)获取参数的值,我们必须遍历每个部分并相应地生成其值。这可以通过以下方式完成:
protected virtual object GetValue()
{
if (IsNullable && IsNull)
{
return null;
}
if (IsString && _value == null)
{
return string.Empty;
}
if (IsParsable)
{
return ParseIfNeeded(_value);
}
if (IsCollection)
{
return GenerateValueFromCollectionElements();
}
if (HasSubFields)
{
return GenerateValueFromSubFields();
}
if (IsEnum)
{
return GenerateEnumValue();
}
return _value;
}
在ParseIfNeeded
方法中,我们使用值的类型的Parse
方法获取值。
在GenerateValueFromCollectionElements
方法中,我们创建一个集合(根据集合的类型),获取集合元素的类型,并将其值设置到集合中。
在GenerateValueFromSubFields
方法中,我们创建一个对象(根据值的类型),并根据SubFields
设置其字段和属性。
在GenerateEnumValue
方法中,我们获取与输入string
表示兼容的Enum
值。
处理方法
呈现方法
有了用于获取值的视图模型之后,我们就可以获取方法的参数,并使用获取的参数调用该方法。为此,我们创建了一个呈现方法的控件。
public class MethodPresenter : Control
{
static MethodPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MethodPresenter),
new FrameworkPropertyMetadata(typeof(MethodPresenter)));
}
}
为此控件,我们添加了用于方法名称的属性。
#region MethodName
public string MethodName
{
get { return (string)GetValue(MethodNameProperty); }
protected set { SetValue(MethodNameProperty, value); }
}
public static readonly DependencyProperty MethodNameProperty =
DependencyProperty.Register("MethodName", typeof(string),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
调用方法的对象。
#region ObjectInstance
public object ObjectInstance
{
get { return (object)GetValue(ObjectInstanceProperty); }
set { SetValue(ObjectInstanceProperty, value); }
}
public static readonly DependencyProperty ObjectInstanceProperty =
DependencyProperty.Register("ObjectInstance", typeof(object),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
并添加了支持派生类型和附加数据模板的属性。
#region KnownTypes
public IEnumerable<Type> KnownTypes
{
get { return (IEnumerable<Type>)GetValue(KnownTypesProperty); }
set { SetValue(KnownTypesProperty, value); }
}
public static readonly DependencyProperty KnownTypesProperty =
DependencyProperty.Register("KnownTypes", typeof(IEnumerable<Type>),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
#region AutoGenerateCompatibleTypes
public bool AutoGenerateCompatibleTypes
{
get { return (bool)GetValue(AutoGenerateCompatibleTypesProperty); }
set { SetValue(AutoGenerateCompatibleTypesProperty, value); }
}
public static readonly DependencyProperty AutoGenerateCompatibleTypesProperty =
DependencyProperty.Register("AutoGenerateCompatibleTypes", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(true));
#endregion
#region DataTemplates
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return (IEnumerable<TypeDataTemplate>)GetValue(DataTemplatesProperty); }
set { SetValue(DataTemplatesProperty, value); }
}
public static readonly DependencyProperty DataTemplatesProperty =
DependencyProperty.Register("DataTemplates", typeof(IEnumerable<TypeDataTemplate>),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
为了获取方法的参数,我们添加了一个用于保存方法参数的属性。
#region MethodParameters
private ObservableCollection<InputValueViewModel> _methodParameters;
public ObservableCollection<InputValueViewModel> MethodParameters
{
get
{
return _methodParameters ??
(_methodParameters = new ObservableCollection<InputValueViewModel>());
}
}
#endregion
创建一个用于获取方法参数的视图模型。
public class ParameterInputValueViewModel : InputValueViewModel
{
public ParameterInputValueViewModel(ParameterInfo pi)
{
ParameterInformation = pi;
}
#region Properties
#region ParameterInformation
private ParameterInfo _parameterInformation;
public ParameterInfo ParameterInformation
{
protected get { return _parameterInformation; }
set
{
if (_parameterInformation != value)
{
ParameterInfo oldValue = _parameterInformation;
_parameterInformation = value;
OnParameterInformationChanged(oldValue, _parameterInformation);
}
}
}
protected virtual void OnParameterInformationChanged
(ParameterInfo oldValue, ParameterInfo newValue)
{
if (newValue == null)
{
return;
}
Name = newValue.Name;
ValueType = newValue.ParameterType;
}
#endregion
#endregion
}
并根据给定的方法信息设置方法的参数。
public MethodInfo MethodInformation
{
get { return (MethodInfo)GetValue(MethodInformationProperty); }
set { SetValue(MethodInformationProperty, value); }
}
public static readonly DependencyProperty MethodInformationProperty =
DependencyProperty.Register("MethodInformation", typeof(MethodInfo),
typeof(MethodPresenter), new UIPropertyMetadata(null, OnMethodInformationChanged));
private static void OnMethodInformationChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
mp.MethodParameters.Clear();
if (mp.MethodInformation != null)
{
mp.MethodName = mp.MethodInformation.Name;
mp.MethodInformation.GetParameters().ToList().
ForEach(pi => mp.MethodParameters.Add(
new ParameterInputValueViewModel(pi)
{
KnownTypes = mp.KnownTypes,
AutoGenerateCompatibleTypes = mp.AutoGenerateCompatibleTypes,
DataTemplates = mp.DataTemplates
}));
}
else
{
mp.MethodName = null;
}
}
调用方法
有了方法的参数之后,我们就必须调用该方法。为此,我们添加了一个RoutedCommand
。
private static RoutedCommand _invokeMethodCommand;
public static RoutedCommand InvokeMethodCommand
{
get
{
return _invokeMethodCommand ??
(_invokeMethodCommand = new RoutedCommand
("InvokeMethod", typeof(MethodPresenter)));
}
}
添加CanExecute
和Executed
事件处理程序。
private static void CanExecuteInvokeMethodCommand
(object sender, CanExecuteRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
e.CanExecute = mp.MethodInformation != null && mp.ObjectInstance != null;
}
private static void ExecuteInvokeMethodCommand(object sender, ExecutedRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
mp.InvokeMethod();
}
private void InvokeMethod()
{
MethodInfo mi = MethodInformation;
object obj = ObjectInstance;
if (mi == null || obj == null)
{
return;
}
object[] parameters = MethodParameters.Select(p => p.Value).ToArray();
_invokeMethodThread = new Thread(() =>
{
OutputValueViewModel methodReturnValue = null;
OutputValueViewModel methodException = null;
try
{
object retVal = mi.Invoke(obj, parameters);
methodReturnValue = mi.ReturnType != typeof(void) ?
new OutputValueViewModel(retVal) : null;
}
catch (System.Reflection.TargetInvocationException tie)
{
methodException = new OutputValueViewModel(tie.InnerException);
}
catch (Exception ex)
{
methodException = new OutputValueViewModel(ex);
}
finally
{
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new ThreadStart(() =>
{
if (methodReturnValue != null)
{
methodReturnValue.DataTemplates = DataTemplates;
}
if (methodException != null)
{
methodException.DataTemplates = DataTemplates;
}
MethodResultViewModel methodResult = new MethodResultViewModel
{
MethodName = mi.Name,
ResultTime = DateTime.Now,
MethodReturnValue = methodReturnValue,
MethodException = methodException
};
int paramInx = 0;
foreach (ParameterInfo pi in mi.GetParameters())
{
if (pi.IsOut || pi.ParameterType.IsByRef)
{
methodResult.MethodOutputs.Add(
new ParameterOutputValueViewModel
(pi, parameters[paramInx])
{
DataTemplates = DataTemplates
});
}
paramInx++;
}
methodResult.Removed += OnMethodResultRemoved;
methodResult.Shown += OnMethodResultShown;
if (StoreMethodResults)
{
MethodResults.Add(methodResult);
HasResults = true;
}
CurrentMethodResult = methodResult;
}));
}
_invokeMethodThread = null;
});
_invokeMethodThread.Start();
}
private Thread _invokeMethodThread;
#region MethodResults
private ObservableCollection<MethodResultViewModel> _methodResults;
public ObservableCollection<MethodResultViewModel> MethodResults
{
get
{
return _methodResults ?? (_methodResults =
new ObservableCollection<MethodResultViewModel>());
}
}
#endregion
#region CurrentMethodResult
public MethodResultViewModel CurrentMethodResult
{
get { return (MethodResultViewModel)GetValue(CurrentMethodResultProperty); }
set { SetValue(CurrentMethodResultProperty, value); }
}
public static readonly DependencyProperty CurrentMethodResultProperty =
DependencyProperty.Register("CurrentMethodResult", typeof(MethodResultViewModel),
typeof(MethodPresenter), new UIPropertyMetadata(null));
#endregion
#region HasResults
public bool HasResults
{
get { return (bool)GetValue(HasResultsProperty); }
protected set { SetValue(HasResultsProperty, value); }
}
public static readonly DependencyProperty HasResultsProperty =
DependencyProperty.Register("HasResults", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(false));
#endregion
#region StoreMethodResults
public bool StoreMethodResults
{
get { return (bool)GetValue(StoreMethodResultsProperty); }
set { SetValue(StoreMethodResultsProperty, value); }
}
public static readonly DependencyProperty StoreMethodResultsProperty =
DependencyProperty.Register("StoreMethodResults", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(false));
#endregion
并将事件处理程序与命令注册。
static MethodPresenter()
{
...
CommandBinding invokeMethodBinding = new CommandBinding(InvokeMethodCommand,
ExecuteInvokeMethodCommand, CanExecuteInvokeMethodCommand);
CommandManager.RegisterClassCommandBinding
(typeof(MethodPresenter), invokeMethodBinding);
}
在InvokeMethod
方法中,我们在另一个线程中获取方法的参数,调用方法(以防止UI阻塞),并使用UI Dispatcher
设置影响UI元素的属性。
为了呈现方法的结果,我们使用MethodResultViewModel
类。该类包含:用于结果详细信息的属性、用于显示结果的命令(此命令触发Shown
事件)以及用于删除结果的命令(此命令触发Removed
事件)。
为了通知用户方法调用,我们添加了一个RoutedEvent
,用于在方法调用之前触发。
public class MethodInvokeRequestedRoutedEventArgs : RoutedEventArgs
{
#region Constructors
public MethodInvokeRequestedRoutedEventArgs()
{
}
public MethodInvokeRequestedRoutedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public MethodInvokeRequestedRoutedEventArgs(RoutedEvent routedEvent, object source)
: base(routedEvent, source)
{
}
#endregion
public MethodInfo MethodInformation { get; set; }
public object[] Parameters { get; set; }
}
public delegate void MethodInvokeRequestedRoutedEventHandler
(object sender, MethodInvokeRequestedRoutedEventArgs e);
public class MethodPresenter : Control
{
...
public static readonly RoutedEvent MethodInvokeRequestedEvent =
EventManager.RegisterRoutedEvent("MethodInvokeRequested", RoutingStrategy.Bubble,
typeof(MethodInvokeRequestedRoutedEventHandler), typeof(MethodPresenter));
public event MethodInvokeRequestedRoutedEventHandler MethodInvokeRequested
{
add { AddHandler(MethodInvokeRequestedEvent, value); }
remove { RemoveHandler(MethodInvokeRequestedEvent, value); }
}
...
}
添加一个RoutedEvent
,用于在方法调用之后触发。
public class MethodInvokedRoutedEventArgs : RoutedEventArgs
{
#region Constructors
public MethodInvokedRoutedEventArgs()
{
}
public MethodInvokedRoutedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
{
}
public MethodInvokedRoutedEventArgs(RoutedEvent routedEvent, object source)
: base(routedEvent, source)
{
}
#endregion
public MethodInfo MethodInformation { get; set; }
public MethodResultViewModel MethodResult { get; set; }
}
public delegate void MethodInvokedRoutedEventHandler
(object sender, MethodInvokedRoutedEventArgs e);
public class MethodPresenter : Control
{
...
public static readonly RoutedEvent MethodInvokedEvent =
EventManager.RegisterRoutedEvent("MethodInvoked", RoutingStrategy.Bubble,
typeof(MethodInvokedRoutedEventHandler), typeof(MethodPresenter));
public event MethodInvokedRoutedEventHandler MethodInvoked
{
add { AddHandler(MethodInvokedEvent, value); }
remove { RemoveHandler(MethodInvokedEvent, value); }
}
...
}
并添加一个属性来指示方法的调用过程。
public bool IsInvoking
{
get { return (bool)GetValue(IsInvokingProperty); }
protected set { SetValue(IsInvokingProperty, value); }
}
public static readonly DependencyProperty IsInvokingProperty =
DependencyProperty.Register("IsInvoking", typeof(bool),
typeof(MethodPresenter), new UIPropertyMetadata(false));
我们在InvokeMethod
方法中适当的引发MethodInvokeRequested
和MethodInvoked
事件,并设置IsInvoking
属性。
为了停止方法的调用,我们添加了一个RoutedCommand
。
private static RoutedCommand _stopInvokeMethodCommand;
public static RoutedCommand StopInvokeMethodCommand
{
get
{
return _stopInvokeMethodCommand ??
(_stopInvokeMethodCommand = new RoutedCommand
("StopInvokeMethod", typeof(MethodPresenter)));
}
}
并实现它以中止方法的调用线程。
private static void CanExecuteStopInvokeMethodCommand
(object sender, CanExecuteRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
e.CanExecute = mp.IsInvoking;
}
private static void ExecuteStopInvokeMethodCommand
(object sender, ExecutedRoutedEventArgs e)
{
MethodPresenter mp = sender as MethodPresenter;
if (mp == null)
{
return;
}
mp.StopInvokeMethod();
}
private void StopInvokeMethod()
{
if (_invokeMethodThread != null)
{
_invokeMethodThread.Abort();
_invokeMethodThread = null;
}
IsInvoking = false;
}
static MethodPresenter()
{
...
CommandBinding stopInvokeMethodBinding = new CommandBinding(StopInvokeMethodCommand,
ExecuteStopInvokeMethodCommand, CanExecuteStopInvokeMethodCommand);
CommandManager.RegisterClassCommandBinding
(typeof(MethodPresenter), stopInvokeMethodBinding);
}
在InvokeMethod
方法中,我们按如下方式处理ThreadAbortException
:
private void InvokeMethod()
{
...
bool isAborted = false;
try
{
object retVal = mi.Invoke(obj, parameters);
methodReturnValue = mi.ReturnType != typeof(void) ?
new OutputValueViewModel(retVal) : null;
}
catch (ThreadAbortException tae)
{
isAborted = true;
}
...
finally
{
if (!isAborted)
{
...
}
}
...
}
MethodPresenter样式
有了MethodPresenter
之后,我们就创建一个默认样式来呈现该控件。
<Style TargetType="{x:Type local:MethodPresenter}">
<Setter Property="Background" Value="LightGreen" />
<Setter Property="BorderBrush" Value="DarkGreen" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Foreground" Value="Black" />
<Setter Property="Padding" Value="15" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MethodPresenter}">
<Grid>
<Border BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="10"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<DockPanel>
</DockPanel>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
在ControlTemplate
中,我们放置一个TextBlock
用于方法名称。
<TextBlock Text="{TemplateBinding MethodName}"
DockPanel.Dock="Top"
FontSize="20"
Foreground="DarkBlue"
HorizontalAlignment="Center"
Margin="5" />
一个ItemsControl
用于方法参数。
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding MethodParameters,
RelativeSource={RelativeSource Mode=TemplatedParent}}" />
</ScrollViewer>
一个Button
用于调用方法。
<Button Content="Invoke" DockPanel.Dock="Bottom"
Command="{x:Static local:MethodPresenter.InvokeMethodCommand}"
HorizontalAlignment="Center"
Margin="5" />
一个Grid
用于方法结果。
<DataTemplate x:Key="methodResultItemDataTemplate">
...
</DataTemplate>
<ControlTemplate TargetType="{x:Type local:MethodPresenter}">
<ControlTemplate.Resources>
<Storyboard x:Key="showResultsStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="1"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="hideResultsStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsBorder"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="showResultsRegionStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="1"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="1"
Duration="0:0:0.2" />
</Storyboard>
<Storyboard x:Key="hideResultsRegionStoryboard">
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleX)"
To="0"
Duration="0:0:0.2" />
<DoubleAnimation Storyboard.TargetName="resultsRegion"
Storyboard.TargetProperty="(FrameworkElement.LayoutTransform).
(ScaleTransform.ScaleY)"
To="0"
Duration="0:0:0.2" />
</Storyboard>
</ControlTemplate.Resources>
...
<Grid DockPanel.Dock="Bottom" x:Name="resultsRegion">
<Grid.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Grid.LayoutTransform>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Button x:Name="btnClear"
Style="{StaticResource removeButtonStyle}"
VerticalAlignment="Center"
Command="{x:Static local:MethodPresenter.
ClearMethodResultsCommand}"
ToolTip="Clear the results."
Margin="2,2,4,2"/>
<ToggleButton x:Name="resultsToggle"
Style="{StaticResource expandButtonStyle}"
VerticalAlignment="Center"
ToolTip="Expand the results region." />
</StackPanel>
<Border x:Name="resultsBorder"
Grid.Row="1"
Margin="0,3,0,0"
BorderThickness="1"
CornerRadius="3"
BorderBrush="#CC000000"
Background="#44000000">
<Border.LayoutTransform>
<ScaleTransform ScaleX="0" ScaleY="0" />
</Border.LayoutTransform>
<StackPanel Margin="5">
<TextBlock Text="Results:"
HorizontalAlignment="Left"
FontSize="14"
Margin="0,0,0,5"
Foreground="#EEFFFFFF"/>
<ScrollViewer MaxHeight="100" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding MethodResults,
RelativeSource={RelativeSource Mode=TemplatedParent}}"
ItemTemplate=
"{StaticResource methodResultItemDataTemplate}" />
</ScrollViewer>
</StackPanel>
</Border>
</Grid>
...
<ControlTemplate.Triggers>
<Trigger SourceName="resultsToggle"
Property="IsChecked"
Value="True">
<Setter TargetName="resultsToggle"
Property="ToolTip"
Value="Collapse the results region." />
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource showResultsStoryboard}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource hideResultsStoryboard}" />
</Trigger.ExitActions>
</Trigger>
<Trigger Property="HasResults"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard=
"{StaticResource showResultsRegionStoryboard}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard=
"{StaticResource hideResultsRegionStoryboard}" />
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
以及一个Border
用于指示方法的调用。
<ControlTemplate TargetType="{x:Type local:MethodPresenter}">
...
<Border Name="invokeDisplay"
CornerRadius="10"
Background="#66000000"
Visibility="Hidden">
<Grid VerticalAlignment="Center"
HorizontalAlignment="Center" >
<Border Background="#BB000000">
<Border.Effect>
<BlurEffect Radius="15" />
</Border.Effect>
</Border>
<StackPanel Margin="10">
<TextBlock Foreground="LightGreen"
FontSize="16"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Text="Invoking..." />
<Button Content="Stop"
Command="{x:Static local:MethodPresenter.
StopInvokeMethodCommand}"
HorizontalAlignment="Center"
Padding="2"
Margin="5" />
</StackPanel>
</Grid>
</Border>
...
<ControlTemplate.Triggers>
<Trigger Property="IsInvoking" Value="True">
<Setter TargetName="invokeDisplay"
Property="Visibility"
Value="Visible" />
</Trigger>
...
</ControlTemplate.Triggers>
</ControlTemplate>
处理接口
呈现接口
有了MethodPresenter
控件之后,我们就可以调用给定接口的方法。为此,我们创建了一个呈现接口的控件。
public class InterfacePresenter : Control
{
static InterfacePresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(InterfacePresenter),
new FrameworkPropertyMetadata(typeof(InterfacePresenter)));
}
}
为此控件,我们添加了一个用于保存调用方法的对象的属性。
public object ObjectInstance
{
get { return (object)GetValue(ObjectInstanceProperty); }
set { SetValue(ObjectInstanceProperty, value); }
}
public static readonly DependencyProperty ObjectInstanceProperty =
DependencyProperty.Register("ObjectInstance", typeof(object),
typeof(InterfacePresenter), new UIPropertyMetadata(null));
并添加了支持派生类型和附加数据模板的属性。
#region KnownTypes
public IEnumerable<Type> KnownTypes
{
get { return (IEnumerable<Type>)GetValue(KnownTypesProperty); }
set { SetValue(KnownTypesProperty, value); }
}
public static readonly DependencyProperty KnownTypesProperty =
DependencyProperty.Register("KnownTypes", typeof(IEnumerable<Type>),
typeof(InterfacePresenter), new UIPropertyMetadata(null));
#endregion
#region AutoGenerateCompatibleTypes
public bool AutoGenerateCompatibleTypes
{
get { return (bool)GetValue(AutoGenerateCompatibleTypesProperty); }
set { SetValue(AutoGenerateCompatibleTypesProperty, value); }
}
public static readonly DependencyProperty AutoGenerateCompatibleTypesProperty =
DependencyProperty.Register("AutoGenerateCompatibleTypes", typeof(bool),
typeof(InterfacePresenter), new UIPropertyMetadata(true));
#endregion
#region DataTemplates
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return (IEnumerable<TypeDataTemplate>)GetValue(DataTemplatesProperty); }
set { SetValue(DataTemplatesProperty, value); }
}
public static readonly DependencyProperty DataTemplatesProperty =
DependencyProperty.Register("DataTemplates", typeof(IEnumerable<TypeDataTemplate>),
typeof(InterfacePresenter), new UIPropertyMetadata(null));
#endregion
为了获取接口的方法,我们添加了一个用于保存接口方法的属性。
private ObservableCollection<MethodInfo> _interfaceMethods;
public ObservableCollection<MethodInfo> InterfaceMethods
{
get
{
return _interfaceMethods ??
(_interfaceMethods = new ObservableCollection<MethodInfo>());
}
}
并根据给定的接口类型设置接口的方法。
public Type InterfaceType
{
get { return (Type)GetValue(InterfaceTypeProperty); }
set { SetValue(InterfaceTypeProperty, value); }
}
public static readonly DependencyProperty InterfaceTypeProperty =
DependencyProperty.Register
("InterfaceType", typeof(Type), typeof(InterfacePresenter),
new UIPropertyMetadata(null,
OnInterfaceTypeChanged), ValidateInterfaceTypeValue);
private static void OnInterfaceTypeChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
InterfacePresenter ip = sender as InterfacePresenter;
if (ip == null)
{
return;
}
ip.InterfaceMethods.Clear();
Type interfaceType = ip.InterfaceType;
if (interfaceType != null)
{
GetInterfaceMethods(interfaceType).ForEach(mi => ip.InterfaceMethods.Add(mi));
}
}
private static bool ValidateInterfaceTypeValue(object value)
{
if (value == null)
{
return true;
}
Type interfaceType = value as Type;
if (interfaceType != null && interfaceType.IsInterface)
{
return true;
}
return false;
}
private static List<MethodInfo> GetInterfaceMethods(Type interfaceType)
{
List<MethodInfo> methodInfos = new List<MethodInfo>();
AddInterfaceMethods(interfaceType, methodInfos);
return methodInfos;
}
private static void AddInterfaceMethods
(Type interfaceType, List<MethodInfo> methodInfos)
{
foreach (MethodInfo mi in interfaceType.GetMethods())
{
methodInfos.Add(mi);
}
foreach (Type baseInterface in interfaceType.GetInterfaces())
{
AddInterfaceMethods(baseInterface, methodInfos);
}
}
为了呈现方法的结果,我们添加了一个用于保存当前方法结果的属性。
public MethodResultViewModel CurrentMethodResult
{
get { return (MethodResultViewModel)GetValue(CurrentMethodResultProperty); }
set { SetValue(CurrentMethodResultProperty, value); }
}
public static readonly DependencyProperty CurrentMethodResultProperty =
DependencyProperty.Register("CurrentMethodResult", typeof(MethodResultViewModel),
typeof(InterfacePresenter), new UIPropertyMetadata
(null, OnCurrentMethodResultChanged));
private static void OnCurrentMethodResultChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
InterfacePresenter ip = sender as InterfacePresenter;
if (ip == null)
{
return;
}
ip.RaiseEvent(new RoutedEventArgs
(InterfacePresenter.CurrentMethodResultChangedEvent));
}
public static readonly RoutedEvent CurrentMethodResultChangedEvent =
EventManager.RegisterRoutedEvent(
"CurrentMethodResultChanged", RoutingStrategy.Bubble,
typeof(RoutedEventHandler), typeof(InterfacePresenter));
public event RoutedEventHandler MethodInvoked
{
add { AddHandler(CurrentMethodResultChangedEvent, value); }
remove { RemoveHandler(CurrentMethodResultChangedEvent, value); }
}
并处理MethodInvoked
事件,将当前结果设置为最后调用方法的那个。
public InterfacePresenter()
{
AddHandler(MethodPresenter.MethodInvokedEvent,
new MethodInvokedRoutedEventHandler(OnMethodInvoked));
}
private void OnMethodInvoked(object sender, MethodInvokedRoutedEventArgs e)
{
CurrentMethodResult = e.MethodResult;
}
InterfacePresenter样式
有了InterfacePresenter
之后,我们就创建一个默认样式来呈现该控件。
<Style TargetType="{x:Type local:InterfacePresenter}">
<Setter Property="Background" Value="LightYellow" />
<Setter Property="BorderBrush" Value="Orange" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Padding" Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:InterfacePresenter}">
<DockPanel>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
在ControlTemplate
中,我们放置一个Border
用于呈现接口的方法。
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="15"
Padding="{TemplateBinding Padding}"
Margin="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Methods:"
Foreground="DarkGreen"
FontSize="14"
FontWeight="Bold"
Margin="0,0,0,5" />
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
Margin="0,5,0,0">
<ItemsControl ItemsSource="{Binding InterfaceMethods,
RelativeSource={RelativeSource Mode=TemplatedParent}}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:MethodPresenter MethodInformation="{Binding}"
ObjectInstance="{Binding DataContext.
ObjectInstance,
ElementName=templatedParentHolder}"
StoreMethodResults="{Binding DataContext.
StoreMethodResults,
ElementName=templatedParentHolder}"
KnownTypes="{Binding DataContext.
KnownTypes,
ElementName=templatedParentHolder}"
AutoGenerateCompatibleTypes=
"{Binding DataContext.
AutoGenerateCompatibleTypes,
ElementName=templatedParentHolder}"
DataTemplates="{Binding DataContext.
DataTemplates,
ElementName=templatedParentHolder}"
Margin="5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<FrameworkElement x:Name="templatedParentHolder"
DataContext="{Binding RelativeSource=
{RelativeSource Mode=TemplatedParent}}"
Visibility="Collapsed" />
</Grid>
</Border>
以及一个Border
用于呈现当前结果。
<DataTemplate x:Key="methodResultDataTemplate">
...
</DataTemplate>
<ControlTemplate TargetType="{x:Type local:InterfacePresenter}">
...
<Border x:Name="currentMethodResultRegion"
DockPanel.Dock="Bottom"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="15"
Padding="{TemplateBinding Padding}"
Margin="5"
RenderTransformOrigin="0.5,0.5">
<Border.BorderBrush>
<SolidColorBrush Color="Orange" />
</Border.BorderBrush>
<Border.RenderTransform>
<ScaleTransform />
</Border.RenderTransform>
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ContentControl Content="{Binding CurrentMethodResult,
RelativeSource={RelativeSource Mode=TemplatedParent}}"
ContentTemplate="{StaticResource methodResultDataTemplate}" />
</ScrollViewer>
</Border>
...
</ControlTemplate>
处理对象
呈现对象的属性
为了支持调用受对象属性影响的方法,我们需要一种方法来设置对象的属性,然后再调用方法。为此,我们创建了一个呈现对象属性的控件。
public class ValuePresenter : Control
{
static ValuePresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ValuePresenter),
new FrameworkPropertyMetadata(typeof(ValuePresenter)));
}
}
为此控件,我们添加了用于值和值类型的属性。
#region Value
public object Value
{
get { return (object)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register("Value", typeof(object),
typeof(ValuePresenter), new UIPropertyMetadata(null));
#endregion
#region ValueType
public Type ValueType
{
get { return (Type)GetValue(ValueTypeProperty); }
set { SetValue(ValueTypeProperty, value); }
}
public static readonly DependencyProperty ValueTypeProperty =
DependencyProperty.Register("ValueType", typeof(Type),
typeof(ValuePresenter), new UIPropertyMetadata(null));
#endregion
并添加了一个用于保存值视图模型的属性。
public InputValueViewModel PresentedValue
{
get { return (InputValueViewModel)GetValue(PresentedValueProperty); }
protected set { SetValue(PresentedValueProperty, value); }
}
public static readonly DependencyProperty PresentedValueProperty =
DependencyProperty.Register("PresentedValue", typeof(InputValueViewModel),
typeof(ValuePresenter), new UIPropertyMetadata(null));
当Value
属性更改时,我们更改值视图模型的值,反之亦然。
为了呈现设置给ValuePresenter
的值,我们需要更新视图模型中每个子视图模型以匹配设置的值。我们可以按如下方式完成:
protected void UpdateValue(object value)
{
object newValue = value;
IsNull = newValue == null;
if (newValue == null)
{
SubFields.Clear();
CollectionElements.Clear();
return;
}
if (SelectedCompatibleType == null || IsCollection || HasSubFields)
{
Type newValueType = newValue != null ? newValue.GetType() : null;
if (ValueType == null)
{
ValueType = newValueType;
}
else if (SelectedCompatibleType != newValueType)
{
SelectedCompatibleType = newValueType;
}
}
if (HasSubFields || IsCollection)
{
UpdateSubFields(newValue);
UpdateCollectionElements(newValue);
}
}
private void UpdateCollectionElements(object newValue)
{
IEnumerable newCollectionValue = newValue as IEnumerable;
if (!IsCollection || newCollectionValue == null)
{
return;
}
int elementIndex = 0;
foreach (object val in newCollectionValue)
{
if (elementIndex >= CollectionElements.Count)
{
AddNewCollectionElement(false);
}
ValueViewModel currElement = CollectionElements[elementIndex];
if (currElement != null)
{
currElement.Value = val;
}
elementIndex++;
}
// Remove surpluses elements.
while (CollectionElements.Count > elementIndex)
{
CollectionElements.RemoveAt(elementIndex);
}
}
private void UpdateSubFields(object newValue)
{
if (newValue == null)
{
return;
}
Type newValueType = newValue.GetType();
foreach (ValueViewModel subField in SubFields)
{
if (subField is PropertyInputValueViewModel)
{
PropertyInfo pi = newValueType.GetProperty(subField.Name);
if (pi != null)
{
subField.Value = pi.GetValue(newValue, null);
}
}
else if (subField is FieldInputValueViewModel)
{
FieldInfo fi = newValueType.GetField(subField.Name);
if (fi != null)
{
subField.Value = fi.GetValue(newValue);
}
}
}
}
在UpdateValue
方法中,我们在需要时更改SelectedCompatibleType
,并相应地更新视图模型中的子视图模型。
ValuePresenter
控件的默认样式定义如下:
<Style TargetType="{x:Type local:ValuePresenter}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ValuePresenter}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<ContentControl Content="{TemplateBinding PresentedValue}" />
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
呈现对象
为了呈现对象,我们创建了一个控件。
public class ObjectPresenter : Control
{
static ObjectPresenter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ObjectPresenter),
new FrameworkPropertyMetadata(typeof(ObjectPresenter)));
}
}
为此控件,我们添加了用于保存我们要呈现的对象的属性。
public object ObjectInstance
{
get { return (object)GetValue(ObjectInstanceProperty); }
set { SetValue(ObjectInstanceProperty, value); }
}
public static readonly DependencyProperty ObjectInstanceProperty =
DependencyProperty.Register("ObjectInstance",
typeof(object), typeof(ObjectPresenter),
new UIPropertyMetadata(null, OnObjectInstanceChanged));
private static void OnObjectInstanceChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
}
并添加了支持派生类型和附加数据模板的属性。
#region KnownTypes
public IEnumerable<Type> KnownTypes
{
get { return (IEnumerable<Type>)GetValue(KnownTypesProperty); }
set { SetValue(KnownTypesProperty, value); }
}
public static readonly DependencyProperty KnownTypesProperty =
DependencyProperty.Register("KnownTypes", typeof(IEnumerable<Type>),
typeof(ObjectPresenter), new UIPropertyMetadata(null));
#endregion
#region AutoGenerateCompatibleTypes
public bool AutoGenerateCompatibleTypes
{
get { return (bool)GetValue(AutoGenerateCompatibleTypesProperty); }
set { SetValue(AutoGenerateCompatibleTypesProperty, value); }
}
public static readonly DependencyProperty AutoGenerateCompatibleTypesProperty =
DependencyProperty.Register("AutoGenerateCompatibleTypes", typeof(bool),
typeof(ObjectPresenter), new UIPropertyMetadata(true));
#endregion
#region DataTemplates
public IEnumerable<TypeDataTemplate> DataTemplates
{
get { return (IEnumerable<TypeDataTemplate>)GetValue(DataTemplatesProperty); }
set { SetValue(DataTemplatesProperty, value); }
}
public static readonly DependencyProperty DataTemplatesProperty =
DependencyProperty.Register("DataTemplates", typeof(IEnumerable<TypeDataTemplate>),
typeof(ObjectPresenter), new UIPropertyMetadata(null));
#endregion
ObjectPresenter
控件的默认样式定义如下:
<Style TargetType="{x:Type local:ObjectPresenter}">
<Setter Property="Background" Value="LightYellow" />
<Setter Property="BorderBrush" Value="Orange" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Padding" Value="10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ObjectPresenter}">
<DockPanel>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="15"
Padding="{TemplateBinding Padding}"
Margin="5"
DockPanel.Dock="Right">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Fields:"
Foreground="DarkGreen"
FontSize="14"
FontWeight="Bold"
Margin="0,0,0,5" />
<ScrollViewer Margin="0,5,0,0"
VerticalScrollBarVisibility="Auto"
Grid.Row="1">
<local:ValuePresenter Value="{Binding ObjectInstance,
RelativeSource={RelativeSource
Mode=TemplatedParent}, Mode=TwoWay}"
KnownTypes="{TemplateBinding
KnownTypes}"
AutoGenerateCompatibleTypes=
"{TemplateBinding
AutoGenerateCompatibleTypes}"
DataTemplates="{TemplateBinding
DataTemplates}" />
</ScrollViewer>
</Grid>
</Border>
<local:InterfacePresenter Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding
BorderThickness}"
Padding="{TemplateBinding Padding}"
InterfaceType=
"{TemplateBinding InterfaceType}"
ObjectInstance="{TemplateBinding
ObjectInstance}"
StoreMethodResults="{TemplateBinding
StoreMethodResults}"
KnownTypes="{TemplateBinding KnownTypes}"
AutoGenerateCompatibleTypes=
"{TemplateBinding
AutoGenerateCompatibleTypes}"
DataTemplates=
"{TemplateBinding DataTemplates}" />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
在此样式中,我们使用InterfacePresenter
控件来呈现对象的***方法,并使用ValuePresenter
控件来呈现对象的***字段。
为了将对象的方法设置到InterfacePresenter
的方法集合中,我们添加了一个TemplatePart
。
[TemplatePart(Name = "PART_InterfacePresenter", Type = typeof(InterfacePresenter))]
public class ObjectPresenter : Control
{
...
}
将InterfacePresenter
的名称设置为TemplatePart
的名称。
<local:InterfacePresenter x:Name="PART_InterfacePresenter"
...
/>
根据名称查找InterfacePresenter
。
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_interfacePresenter = GetTemplateChild("PART_InterfacePresenter")
as InterfacePresenter;
}
private InterfacePresenter _interfacePresenter;
并更新InterfacePresenter
的方法集合,每次ObjectInstance
更改时。
private static void OnObjectInstanceChanged
(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
ObjectPresenter op = sender as ObjectPresenter;
if (op == null)
{
return;
}
if (e.OldValue != null && e.NewValue != null &&
e.OldValue.GetType() == e.NewValue.GetType())
{
// The new object has the same type as the previous,
// so we don't have to regenerate methods.
return;
}
if (op.InterfaceType == null)
{
op.UpdateInterfacePresenterWithObjectInstanceMethods();
}
}
private void UpdateInterfacePresenterWithObjectInstanceMethods()
{
if (_interfacePresenter == null)
{
return;
}
_interfacePresenter.InterfaceMethods.Clear();
if (ObjectInstance == null)
{
return;
}
IEnumerable<methodinfo /> methods = ObjectInstance.GetType().GetMethods();
foreach (MethodInfo mi in methods)
{
if (mi.IsSpecialName)
{
continue;
}
_interfacePresenter.InterfaceMethods.Add(mi);
}
}
如何使用它
使用ValuePresenter
为了演示ValuePresenter
控件的用法,我们创建了一个允许编辑形状属性的窗口。
对于该窗口,我们创建了一个用于保存形状的基类。
public abstract class MyShape
{
public MyShape()
{
Left = 10;
Top = 10;
FillColor = Colors.LightGreen;
StrokeColor = Colors.Green;
StrokeThickness = 2;
}
public double Left { get; set; }
public double Top { get; set; }
public Color FillColor { get; set; }
public Color StrokeColor { get; set; }
public double StrokeThickness { get; set; }
}
创建用于保存矩形和圆形的派生类。
public class MyRectangle : MyShape
{
public MyRectangle()
{
Width = 150;
Height = 200;
}
public double Width { get; set; }
public double Height { get; set; }
}
public class MyCircle : MyShape
{
public MyCircle()
{
Diameter = 100.0;
}
public double Diameter { get; set; }
}
创建用于呈现形状的数据模板。
<DataTemplate DataType="{x:Type local:MyRectangle}">
<Canvas>
<Rectangle Canvas.Top="{Binding Top}"
Canvas.Left="{Binding Left}"
StrokeThickness="{Binding StrokeThickness}"
Width="{Binding Width}"
Height="{Binding Height}" >
<Rectangle.Fill>
<SolidColorBrush Color="{Binding FillColor}" />
</Rectangle.Fill>
<Rectangle.Stroke>
<SolidColorBrush Color="{Binding StrokeColor}" />
</Rectangle.Stroke>
</Rectangle>
</Canvas>
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyCircle}">
<Canvas>
<Ellipse Canvas.Top="{Binding Top}"
Canvas.Left="{Binding Left}"
StrokeThickness="{Binding StrokeThickness}"
Width="{Binding Diameter}"
Height="{Binding Diameter}" >
<Ellipse.Fill>
<SolidColorBrush Color="{Binding FillColor}" />
</Ellipse.Fill>
<Ellipse.Stroke>
<SolidColorBrush Color="{Binding StrokeColor}" />
</Ellipse.Stroke>
</Ellipse>
</Canvas>
</DataTemplate>
添加一个ValuePresenter
用于编辑形状的属性。
<Border Grid.Column="1"
BorderBrush="Blue"
BorderThickness="2"
Background="LightBlue"
CornerRadius="5"
Margin="5" Padding="5">
<ObjectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}" />
</Border>
并添加一个ContentControl
用于呈现ValuePresenter
的值(形状)。
<ContentControl Name="myContent"
Content="{Binding ElementName=vp, Path=Value, Mode=TwoWay}" />
为了呈现颜色,我们创建了一个UserControl
。
<UserControl x:Class="ObjectPresentation.Examples.Client.ColorPicker"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="R:" VerticalAlignment="Center" />
<Slider Name="rVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Margin="0,2" ValueChanged="OnValueChanged" />
<TextBlock Text="G:" Grid.Row="1" VerticalAlignment="Center"/>
<Slider Name="gVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Grid.Row="1" Margin="0,2"
ValueChanged="OnValueChanged" />
<TextBlock Text="B:" Grid.Row="2" VerticalAlignment="Center" />
<Slider Name="bVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Grid.Row="2" Margin="0,2"
ValueChanged="OnValueChanged" />
<TextBlock Text="A:" Grid.Row="3" VerticalAlignment="Center" />
<Slider Name="aVal" Minimum="0" Maximum="255" Value="60"
Grid.Column="1" Grid.Row="3" Margin="0,2"
ValueChanged="OnValueChanged" />
<Grid Grid.Column="2" Grid.RowSpan="4" Margin="5" >
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle>
<Rectangle.Fill>
<LinearGradientBrush SpreadMethod="Repeat" StartPoint="0.5,0"
EndPoint="0.5,0.2" >
<GradientStop Color="White" Offset="0" />
<GradientStop Color="White" Offset="0.5" />
<GradientStop Color="LightGray" Offset="0.5" />
<GradientStop Color="LightGray" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Column="1">
<Rectangle.Fill>
<LinearGradientBrush SpreadMethod="Repeat" StartPoint="0.5,0"
EndPoint="0.5,0.2" >
<GradientStop Color="LightGray" Offset="0" />
<GradientStop Color="LightGray" Offset="0.5" />
<GradientStop Color="White" Offset="0.5" />
<GradientStop Color="White" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.ColumnSpan="2"
Stroke="Black"
StrokeThickness="1"
Width="16" >
<Rectangle.Fill>
<SolidColorBrush Color="{Binding RelativeSource=
{RelativeSource Mode=FindAncestor,
AncestorType={x:Type UserControl}}, Path=SelectedColor}" />
</Rectangle.Fill>
</Rectangle>
</Grid>
</Grid>
</UserControl>
public partial class ColorPicker : UserControl
{
public ColorPicker()
{
InitializeComponent();
rVal.Value = SelectedColor.R;
gVal.Value = SelectedColor.G;
bVal.Value = SelectedColor.B;
aVal.Value = SelectedColor.A;
}
#region SelectedColor
public Color SelectedColor
{
get { return (Color)GetValue(SelectedColorProperty); }
set { SetValue(SelectedColorProperty, value); }
}
public static readonly DependencyProperty SelectedColorProperty =
DependencyProperty.Register("SelectedColor", typeof(Color),
typeof(ColorPicker), new UIPropertyMetadata(Colors.Transparent,
OnSelectedColorChanged));
private static void OnSelectedColorChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
ColorPicker cp = sender as ColorPicker;
if (cp == null)
{
return;
}
Color newColor = cp.SelectedColor;
cp.rVal.Value = newColor.R;
cp.gVal.Value = newColor.G;
cp.bVal.Value = newColor.B;
cp.aVal.Value = newColor.A;
}
#endregion
private void OnValueChanged(object sender, RoutedPropertyChangedEventArgs<double /> e)
{
if (!IsLoaded)
{
return;
}
SelectedColor = new Color
{
R = (byte)rVal.Value,
G = (byte)gVal.Value,
B = (byte)bVal.Value,
A = (byte)aVal.Value
};
}
}
创建一个使用此UserControl
为Color
类型的数据模板。
<Grid.Resources>
<x:ArrayExtension x:Key="typeDataTemplates"
Type="{x:Type ObjectPresentation:TypeDataTemplate}">
<ObjectPresentation:TypeDataTemplate ValueType="{x:Type Color}">
<ObjectPresentation:TypeDataTemplate.ValueViewModelDataTemplate>
<DataTemplate>
<local:ColorPicker Width="150"
SelectedColor="{Binding Path=Value,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</ObjectPresentation:TypeDataTemplate.ValueViewModelDataTemplate>
</ObjectPresentation:TypeDataTemplate>
</x:ArrayExtension>
</Grid.Resources>
并将数据模板设置在ValuePresenter
中。
<ObjectPresentation:ValuePresenter x:Name="vp"
ValueType="{x:Type local:MyShape}"
DataTemplates="{StaticResource typeDataTemplates}" />
结果可以如下显示:

使用MethodPresenter
为了演示MethodPresenter
控件的用法,我们创建了一个允许将形状添加到Canvas
的窗口。
对于该窗口,我们添加了一个Canvas
用于保存形状。
<Canvas Name="myCanvas" />
添加一个MethodPresenter
。
<ObjectPresentation:MethodPresenter Name="myMethodPresenter"
Grid.Column="1"
DataTemplates="{StaticResource typeDataTemplates}"
Margin="5" />
添加一个用于添加形状的方法(使用我们在上一个示例中创建的形状)。
public void AddShape(MyShape shape)
{
if (shape == null)
{
return;
}
Dispatcher.BeginInvoke(DispatcherPriority.Normal,
new ThreadStart(() =>
{
Shape element = null;
if (shape is MyCircle)
{
MyCircle mc = shape as MyCircle;
element = new Ellipse
{
Width = mc.Diameter,
Height = mc.Diameter
};
}
else if (shape is MyRectangle)
{
MyRectangle mr = shape as MyRectangle;
element = new Rectangle
{
Width = mr.Width,
Height = mr.Height,
};
}
if (element != null)
{
element.Fill = new SolidColorBrush(shape.FillColor);
element.Stroke = new SolidColorBrush(shape.StrokeColor);
element.StrokeThickness = shape.StrokeThickness;
Canvas.SetLeft(element, shape.Left);
Canvas.SetTop(element, shape.Top);
myCanvas.Children.Add(element);
}
}));
}
并将MethodPresenter
的MethodInformation
设置为该方法。
public MethodPresenterExample()
{
InitializeComponent();
myMethodPresenter.MethodInformation = this.GetType().GetMethod("AddShape");
myMethodPresenter.ObjectInstance = this;
}
结果可以如下显示:

使用InterfacePresenter
为了演示InterfacePresenter
控件的用法,我们创建了一个WCF服务和一个调用该服务方法的GUI客户端。
对于服务,我们创建了一些数据契约。
[DataContract]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
[DataMember]
public PersonGender Gender { get; set; }
[DataMember]
public Address Address { get; set; }
[DataMember]
public List<Person> Children { get; set; }
}
[DataContract]
public enum PersonGender
{
[EnumMember]
Male,
[EnumMember]
Female
}
[DataContract]
public class Address
{
[DataMember]
public string Country { get; set; }
[DataMember]
public string City { get; set; }
[DataMember]
public string Street { get; set; }
[DataMember]
public int Number { get; set; }
}
创建了一个服务契约。
[ServiceContract]
public interface IMyService
{
[OperationContract]
void AddPerson(Person person);
[OperationContract]
Person GetPersonByName(string name);
[OperationContract]
Person[] GetAllPersons();
[OperationContract]
double Add(double a, double b);
[OperationContract]
double Sub(double a, double b);
[OperationContract]
double Mul(double a, double b);
[OperationContract]
double Div(double a, double b);
}
创建了一个实现了服务契约的类。
public class MyService : IMyService
{
private List<Person> _persons;
public MyService()
{
_persons = new List<Person>();
}
public void AddPerson(Person person)
{
if (person != null)
{
_persons.Add(person);
}
}
public Person GetPersonByName(string name)
{
return _persons.FirstOrDefault(p => p.Name == name);
}
public Person[] GetAllPersons()
{
return _persons.ToArray();
}
public double Add(double a, double b)
{
return a + b;
}
public double Sub(double a, double b)
{
return a - b;
}
public double Mul(double a, double b)
{
return a * b;
}
public double Div(double a, double b)
{
return a / b;
}
}
添加了服务配置。
<system.serviceModel>
<services>
<service name="ObjectPresentation.Examples.Service.MyService">
<endpoint address="net.tcp://:8123/MyService"
binding="netTcpBinding"
contract="ObjectPresentation.Examples.Contracts.IMyService" />
</service>
</services>
</system.serviceModel>
并使用服务的类创建了一个ServiceHost
。
ServiceHost host = new ServiceHost(typeof(MyService));
host.Open();
Console.WriteLine("Press <Enter> to stop.");
Console.ReadLine();
host.Close();
为了显示到达服务方法的输入和结果,我们可以使用我的LoggingBehavior,如下所示:
[LoggingBehavior]
public class MyService : IMyService
{
...
}
对于客户端,我们添加了服务配置。
<system.serviceModel>
<client>
<endpoint address="net.tcp://:8123/MyService"
binding="netTcpBinding"
contract="ObjectPresentation.Examples.Contracts.IMyService"
name="myServiceEp" />
</client>
</system.serviceModel>
添加了一个InterfacePresenter
。
<ObjectPresentation:InterfacePresenter x:Name="myInterfacePresenter" />
并将其ObjectInstance
设置为服务的代理。
public InterfacePresenterExample()
{
InitializeComponent();
ChannelFactory<IMyService> factory = new ChannelFactory<IMyService>("myServiceEp");
IMyService proxy = factory.CreateChannel();
myInterfacePresenter.InterfaceType = typeof(IMyService);
myInterfacePresenter.ObjectInstance = proxy;
}
结果可以如下显示:


使用ObjectPresenter
为了演示ObjectPresenter
控件的用法,我们在Person
类中添加了一些方法。
[DataContract]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
[DataMember]
public PersonGender Gender { get; set; }
[DataMember]
public Address Address { get; set; }
[DataMember]
public List<Person> Children { get; set; }
public string[] GetChildrenNames()
{
if (Children == null)
{
return null;
}
return Children.Select(c => c.Name).ToArray();
}
public Person GetChildByName(string name)
{
// Throws exception when Children==null.
return Children.FirstOrDefault(c => c.Name == name);
}
public void AddChild(Person child)
{
if (Children == null)
{
Children = new List<Person>();
}
Children.Add(child);
}
public void SetAddress(Address address)
{
this.Address = address;
}
}
添加了一个ObjectPresenter
。
<ObjectPresentation:ObjectPresenter Name="myObjectPresenter" />
并将其ObjectInstance
设置为一个Person
对象。
public ObjectPresenterExample()
{
InitializeComponent();
myObjectPresenter.ObjectInstance = new Person();
}
结果可以如下显示:
