C# 中 Windows 8 Metro 风格的附加命令
C# 中 Windows 8 Metro 风格的附加命令
引言
Windows 8 开发者预览版和 Blend 5 开发者预览版还不支持 EventToCommand 行为(MVVM Light Toolkit 可用于 Metro:http://mvvmlight.codeplex.com 但 Blend 行为不可用)。 许多开发者希望在他们的 MVVM 模式中实现事件到命令的绑定。所以我们需要 Metro 的附加命令(类似于 WPF 中的 AttachedCommand)。
设计代码
好,让我们开始。
通常,在 MVVM 模式中,我们喜欢在 ViewModel 中声明值属性和命令,并在 View 上使用 DataBinding。 这样就可以完成“数据驱动 UI”的工作。 让我们看看 Metro 风格的应用程序。 虽然它使用 XAML 和 WinRT 组件,但它仍然可以使用 MVVM 模式作为其架构。 Metro 仍然支持 DataBinding,我们仍然可以让“数据”驱动“Metro UI”。然而,在 ViewModel 中实现属性和命令很容易,但将一个 UI 控件事件分配给一个命令并不容易。 在 WPF 中,我们可以使用 Blend SDK 行为将一个事件分配给一个命令,或者我通常建议设计一个 AttachedCommand,就像这样: http://marlongrech.wordpress.com/2008/12/13/attachedcommandbehavior-v2-aka-acb 。但是查看 Metro 的 MVVM Light Toolkit,它不能提供 EventToCommand 行为,并且 Blend 5 开发者预览版也没有提供该行为。 因此,我们只需要为 Metro 设计一个 AttachedCommand。
在我们的 AttachedCommand 类中,我们应该声明两个附加属性,Command 和 RoutedEvent 属性。
/// <summary>
/// Command attached property
/// </summary>
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
"Object", // should be "Object" instead of "ICommand" interface, resolve the null reference exception
typeof(AttachedCommand).FullName, new PropertyMetadata(DependencyProperty.UnsetValue));
public static ICommand GetCommand(DependencyObject d)
{
return (ICommand)d.GetValue(CommandProperty);
}
public static void SetCommand(DependencyObject d, ICommand value)
{
d.SetValue(CommandProperty, value);
}
/// <summary>
/// RoutedEvent property
/// </summary>
public static readonly DependencyProperty RoutedEventProperty =
DependencyProperty.RegisterAttached("RoutedEvent",
"String",
typeof(AttachedCommand).FullName,
new PropertyMetadata(String.Empty, new PropertyChangedCallback(OnRoutedEventChanged)));
public static String GetRoutedEvent(DependencyObject d)
{
return (String)d.GetValue(RoutedEventProperty);
}
public static void SetRoutedEvent(DependencyObject d, String value)
{
d.SetValue(RoutedEventProperty, value);
}
我们应该通过类型的字符串在 Metro 中注册附加属性(类似于 Silverlight 解决方案),对于 Interface,我们应该使用“Object”代替。 不确定这是否会在下一个 Windows 8 版本中解决,但对于 interface,它会抛出 NullReferenceException。
下面是 RoutedEvent 属性的属性更改回调
private static void OnRoutedEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
String routedEvent = (String)e.NewValue;
if (!String.IsNullOrEmpty(routedEvent))
{
EventHooker eventHooker = new EventHooker();
eventHooker.AttachedCommandObject = d;
EventInfo eventInfo = GetEventInfo(d.GetType(), routedEvent);
if (eventInfo != null)
{
eventInfo.AddEventHandler(d, eventHooker.GetEventHandler(eventInfo));
}
}
}
}
我们需要通过反射 Metro DependecyObject 来获取 EventInfo。 但是在 Metro 中,反射只能列出直接在当前类型中声明的成员。 它无法列出从基类型继承的所有成员。 因此,我们应该使用一种方法从其基类型中搜索事件成员
/// <summary>
/// Search the EventInfo from the type and its base types
/// </summary>
/// <param name="type"></param>
/// <param name="eventName"></param>
/// <returns></returns> <returns />
private static EventInfo GetEventInfo(Type type, string eventName)
{
EventInfo eventInfo = null;
eventInfo = type.GetTypeInfo().GetDeclaredEvent(eventName);
if (eventInfo == null)
{
Type baseType = type.GetTypeInfo().BaseType;
if (baseType != null)
return GetEventInfo(type.GetTypeInfo().BaseType, eventName);
else
return eventInfo;
}
return eventInfo;
}
当控件上触发特定事件时,我们应该返回事件处理程序。 因此,有一个 EventHooker 可以返回一个“OnEventRaised”方法,并在其中执行命令
internal sealed class EventHooker
{
public DependencyObject AttachedCommandObject { get; set; }
public Delegate GetEventHandler(EventInfo eventInfo)
{
Delegate del = null;
if (eventInfo == null)
throw new ArgumentNullException("eventInfo");
if (eventInfo.EventHandlerType == null)
throw new ArgumentNullException("eventInfo.EventHandlerType");
if (del == null)
del = this.GetType().GetTypeInfo().GetDeclaredMethod("OnEventRaised").CreateDelegate(eventInfo.EventHandlerType, this);
return del;
}
private void OnEventRaised(object sender, object e) // the second parameter in Windows.UI.Xaml.EventHandler is Object
{
ICommand command = (ICommand)(sender as DependencyObject).GetValue(AttachedCommand.CommandProperty);
if (command != null)
command.Execute(null);
}
}
如何使用
我们应该为 Metro 添加一个 DelegateCommand 或 RelayCommand(ICommand),它可以帮助我们在 ViewModel 中返回 ICommand。 我们可以将此 ICommand 属性绑定到 AttachedCommand.Command 属性。 下面是 Metro 的一个 DelegateCommand。 MVVM Light Toolkit 也提供了 Metro 可用的 RelayCommand。
public class DelegateCommand : ICommand
{
private readonly Predicate<object> _canExecute;
private readonly Action<object> _execute;
public event Windows.UI.Xaml.EventHandler CanExecuteChanged;
public DelegateCommand(Action<object> execute)
: this(execute, null)
{
}
public DelegateCommand(Action<object> execute,
Predicate<object> canExecute)
{
_execute = execute;
_canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
if (_canExecute == null)
return true;
return _canExecute(parameter);
}
public void Execute(object parameter)
{
_execute(parameter);
}
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, null);
}
}
}
请注意,WinRT EventHandler 与 System.EventHandler 不同。 并且 Windows.UI.Xaml.EventHandler 有两个对象参数,第二个不是 EventArgs。
然后在 View 中,我们只需在 Metro 中的任何控件中设置 AttachedCommand
<Button Content="Test Button"
local:AttachedCommand.RoutedEvent="PointerEntered" local:AttachedCommand.Command="{Binding TestCommand}"/>