WPF:翻转瓷砖 3D






4.91/5 (57投票s)
WPF:Onyx WPF框架使用演示。
目录
引言
我从事WPF开发大约两年了,无论是出于兴趣还是工作需要,我一直关注着WPF模式的演变,并且有幸接触到WPF领域里一些重量级人物。似乎大多数聪明的WPF开发者都采用了一种叫做ModelView-ViewModel (简称MVVM) 的模式。这种模式允许将视图抽象到ViewModel,这样视图就是被动的,仅仅通过WPF绑定(使用WPF Binding)来绑定到相关的ViewModel。这种模式的优点在于,如果你正确地实现了MVVM,那么你就可以在不需要实际UI的情况下测试你的UI。你也可以测试ViewModel。事实上,你甚至可以在不看视图的情况下检查应用程序的ViewModels,就能了解发生了什么。
现在,MVVM WPF开发中的圣杯是完全不写代码隐藏,让视图仅仅绑定到ViewModel。不幸的是,总有一些事情会阻碍这种美好。例如,ViewModel如何显示消息框或禁用控件?这是一个很大的挑战,但幸运的是,WPF提供了帮助,通过使用路由命令(routed commanding),它允许ViewModels拥有命令,而支持ICommand
接口的UI控件可以执行这些命令。那么消息框/对话框(如OpenFileDialog/SaveFileDialog等)以及类似的东西怎么办?ViewModel如何处理这些?
嗯,使用WPF原生功能并不是那么容易,你需要付出一些努力。如果你不这样做,你最终会得到一堆不可测试的代码隐藏。相信我,这种情况很容易发生,有时我也会遇到,这让我非常恼火,因为我正在打破我认为很好的模式。GRRR...
所以,你要么自己做很多自定义工作,要么让别人为你付出努力并使用他们的成果。运气好的话,我的一位WPF Disciples的朋友,Bill Kempf,在他称之为Onyx的酷炫MVVM WPF框架上做了一些很棒的工作。Bill还没有完成Onyx框架,它还在不断发展,但我只想给它一些曝光,看看它现在能做什么。
所以在这篇文章中,我将讨论Bill的相当巧妙的MVVM WPF框架能做什么。如果你对此感兴趣,请继续阅读。
查看演示
好的,那么演示应用程序看起来是什么样的?嗯,我本来想做一些相当枯燥的东西,这样就能清楚哪些部分是Onyx框架,哪些部分不是,但我就是忍不住,不得不构思出一些让我满意的东西。所以我设计了一个小的3D应用程序,可以让你在3D空间中查看一些WPF Disciples,并且翻转3D模型以显示你选择的WPF Disciples的大图,它还会使用.NET 3.5的WebBrowser
控件向你展示他们的博客。
所以这里有六张截图
我非常清楚这是一个相当无用的应用程序,我对此没有丝毫幻想。演示应用程序不是重点,它只是一个简单的演示,而真正重要的是实际的Onyx框架,所以这篇文章实际上是关于这个的。我认为我的工作是传播一些能帮助你的东西,我认为Onyx框架会帮助你。Bill做得好。好了,现在我们开始写文章的重点内容,好吗?
必备组件
运行此代码真正需要的是Onyx DLL和.NET 3.5 SP1,这些都包含在下载的代码中,但如果你想深入了解它的内部结构,你还需要Bill的另一个开源项目Specificity,Onyx使用它进行测试。这个也值得一看。我很少被某个人的代码所折服,但我真的很喜欢Bill的两个开源项目。
深入了解Onyx框架
本节将介绍Onyx如何帮助你进行日常的WPF MVVM开发。需要指出的是,我不是Onyx的作者,所以我在这里提到的任何内容都是通过我查看Bill编写的源代码得出的。
设置ViewModel
Onyx有一个要求,你必须使用附加依赖属性(Attached DependencyProperty)将特定的视图与其关联的ViewModel关联起来。这很容易做到,如下所示:
<Window x:Class="FlipTile3D.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:onyx="http://schemas.onyx.com/2009/fx/presentation"
xmlns:local="clr-namespace:FlipTile3D"
onyx:View.Model="{x:Type local:MainWindowViewModel}">
这里实际发生了什么?嗯,实际上发生了不少事。onyx:View.Model
附加DP被声明为:
public static readonly DependencyProperty ModelProperty =
DependencyProperty.RegisterAttached(
"Model",
typeof(object),
typeof(View),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.Inherits,
OnModelChanged, CoerceModel));
/// <summary>
/// Called when the <see cref="ModelProperty"/> attached dependency property is
/// changed on the <paramref name="source"/>.
/// </summary>
/// <param name="source">The source object.</param>
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/>
/// instance containing the event data.</param>
private static void OnModelChanged(DependencyObject source,
DependencyPropertyChangedEventArgs e)
{
if (source.AsFrameworkObject() == null)
{
return;
}
object model = e.NewValue;
if (model == null)
{
return;
}
if (DependencyPropertyHelper.GetValueSource(source,
ModelProperty).BaseValueSource == BaseValueSource.Local &&
DependencyPropertyHelper.GetValueSource(source,
FrameworkElement.DataContextProperty).BaseValueSource != BaseValueSource.Local)
{
source.SetValue(FrameworkElement.DataContextProperty, model);
}
}
当设置onyx:View.Model
附加DP时,Onyx在后台所做的是,将关联视图(onyx:View.Model
附加DP的源)的DataContext
属性设置为赋给onyx:View.Model
附加DP的值的ViewModel。
这一个属性就足以设置视图与其关联ViewModel之间的所有绑定。
命令支持
Onyx使用了WPF标准的路由命令(RoutedCommands)机制。路由命令允许你分离命令和实现。例如,ViewModel可以公开路由命令,然后视图可以绑定到这些命令,命令的实现将由ViewModel完成,这再次实现了清晰可测试的代码。
Onyx如何与命令协同工作:你只需要公开一个命令,并在ViewModel中定义命令的实现:
public class MainWindowViewModel : ViewModel
{
#region Ctor
/// <summary>
/// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
/// </summary>
/// <param name="view">The view to associate with this instance.</param>
public MainWindowViewModel(View view)
: base(view)
{
this.CommandBindings.Add(new CommandBinding(OpenDisciplesXMLFileCommand,
this.OpenDisciplesXMLFileExecuted,
new CanExecuteRoutedEventHandler(this.OpenDisciplesXMLFileCanExecute)));
}
#endregion
#region Commands
/// <summary>
/// The OpenDisciplesXMLFile routed command.
/// </summary>
private static readonly RoutedUICommand cmdOpenDisciplesXMLFile =
new System.Windows.Input.RoutedUICommand(
"Open Disciples XML File",
"OpenDisciplesXMLFileCommand",
typeof(MainWindowViewModel));
/// <summary>
/// Gets the OpenDisciplesXMLFile command.
/// </summary>
/// <value>The OpenDisciplesXMLFile command.</value>
public static RoutedUICommand OpenDisciplesXMLFileCommand
{
get { return cmdOpenDisciplesXMLFile; }
}
/// <summary>
/// Determines if the OpenDisciplesXMLFileCommand can Execute
/// </summary>
private void OpenDisciplesXMLFileCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
//Allow command to run
e.CanExecute = true;
}
/// <summary>
/// Handles the execution of the <see cref="MainWindow.OpenDisciplesXMLFile"/> command.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="args">The <see cref="System.Windows.RoutedEventArgs"/>
/// instance containing the event data.</param>
private void OpenDisciplesXMLFileExecuted(object sender, RoutedEventArgs args)
{
//DO SOMETHING
//DO SOMETHING
}
}
所以,我们只需要将视图连接到公开的ViewModel命令。这是一个例子:
<Button x:Name="btnOpenFile"
Command="local:MainWindowViewModel.OpenDisciplesXMLFileCommand"/>
就是这样。这意味着ViewModel拥有实际的命令及其实现,而视图则简单地绑定到这些公开的命令。这也实现了单元测试,单元测试可以像这样执行ViewModel命令:
ViewModel x = new ViewModel();
x.SomeCommand.Execute();
服务定位器
Onyx的核心是一个服务提供商(ServiceProvider)。在内部,它注册新服务,然后可以使用Microsoft的System.IServiceProvider
接口来获取这些服务,该接口定义如下:
namespace System
{
// Summary:
// Defines a mechanism for retrieving
// a service object; that is, an object that
// provides custom support to other objects.
public interface IServiceProvider
{
// Summary:
// Gets the service object of the specified type.
//
// Parameters:
// serviceType:
// An object that specifies the type of service object to get.
//
// Returns:
// A service object of type serviceType.
// -or- null if there is no service object
// of type serviceType.
object GetService(Type serviceType);
}
}
发生的事情是,Onyx View对象用于公开其服务,这在内部被转换为调用System.IServiceProvider.GetService()
方法。以下是在Onyx View对象内部完成此操作的方法:
/// <summary>
/// Gets the service object of the specified type.
/// </summary>
/// <param name="serviceType">An object that
/// specifies the type of service object to get.</param>
/// <returns>
/// A service object of type <paramref name="serviceType"/>.
/// -or-
/// null if there is no service object of type <paramref name="serviceType"/>.
/// </returns>
/// <exception cref="ObjectDisposedException">This instance has been disposed.</exception>
public object GetService(Type serviceType)
{
if (serviceType == null)
{
throw new ArgumentNullException(Reflect.GetField(() => serviceType).Name);
}
return this.serviceContainer.GetService(serviceType);
}
现在我们知道可以为视图获取服务。Onyx提供什么样的服务?嗯,为了理解这一点,我们需要看看Onyx View对象最初是如何创建的;以下是发生的情况:
/// <summary>
/// Gets the view.
/// </summary>
/// <param name="source">The source.</param>
/// <returns>
/// The <see cref="View"/> associated with the <paramref name="source"/>.
/// </returns>
/// <remarks>
/// If there currently is no <see cref="View"/>
/// associated with the <paramref name="source"/>
/// then a new <see cref="View"/> is created.
/// </remarks>
/// <exception cref="ArgumentNullException">
/// <paramref name="source"/> is null.</exception>
public static View GetView(DependencyObject source)
{
if (source == null)
{
throw new ArgumentNullException(Reflect.GetField(() => source).Name);
}
View view = (View)source.GetValue(ViewProperty);
if (view == null)
{
view = new View(source);
source.SetValue(ViewProperty, view);
IServiceRegistry container =
View.ServiceRegistryActivator.CreateServiceRegistry();
View.ViewCreated(view, new ViewCreatedEventArgs(view.ViewElement, container));
container.RegisterServicesFor(view.ViewElement);
view.serviceContainer = container.CreateServiceProvider();
}
return view;
}
可以看出,此方法首先检查Onyx View对象是否需要实例化,如果需要创建Onyx View对象,这时就会注册所有服务。这又回到了注册了哪些服务以及哪些服务可用的问题。这里的关键在于查看ViewCreated
事件的工作方式。让我们看看当Onyx View创建时运行的结果代码。
/// <summary>
/// Called when a <see cref="View"/> is created.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="ViewCreatedEventArgs"/>
/// instance containing the event data.</param>
/// <remarks>
/// Adds common framework services to the <see cref="View"/>.
/// </remarks>
private static void OnViewCreated(object sender, ViewCreatedEventArgs e)
{
UIElement uiElement = e.ViewElement as UIElement;
if (uiElement != null)
{
UIElementServices services = new UIElementServices(uiElement);
services.AddServices(e.ServiceContainer);
}
IFrameworkObject frameworkObject = e.ViewElement.AsFrameworkObject();
if (frameworkObject != null)
{
CommonFrameworkElementServices services =
new CommonFrameworkElementServices(frameworkObject);
services.AddServices(e.ServiceContainer);
}
}
可以看出,Onyx使用UIElementServices
对象和CommonFrameworkElementServices
对象来添加服务。现在让我们检查这两个Onyx对象。
UIElementServices
internal class UIElementServices : ServicesBase, ICommandTarget,
IFocusSuggested, IDisplayMessage, ICommonDialogProvider
这个Onyx对象提供以下服务:
- 焦点支持
- 消息框支持
- OpenFileDialog支持
- SaveFileDialog支持
CommonFrameworkElementServices
internal class CommonFrameworkElementServices : ServicesBase, IElementLifetime
这个Onyx对象提供以下服务:
- 对象生命周期支持,如初始化/创建/加载。
目前,重要的是要知道Onyx View对象公开服务,并且你可以从ViewModel中使用这些服务。我现在将继续展示如何使用一些典型的Onyx服务。
消息框(服务)
当人们尝试创建典型的WPF MVVM应用程序时,他们会遇到一个问题,那就是进行到一半时,ViewModel中出现了一些错误,他们需要告知用户。哦,糟糕。这通常意味着需要向用户显示一个消息框。所以,你可以创建一个新的消息框显示接口在视图上,让ViewModel通过这个新接口与视图通信;或者,你可以忽略ViewModel应该是UI无关的事实,直接让它引用必要的DLL,让ViewModel向用户显示消息框。
这两种方法都有效,但第二种感觉不太对。特定的接口方法听起来更好。幸运的是,Onyx很好地处理了这种情况。
要从ViewModel向用户显示消息框,我们只需获取正确的服务类型(视图实现的接口),并在ViewModel中使用此获取的服务执行某些操作。
Onyx将消息框服务公开为IDisplayMessage
,可以按如下方式使用:
var messageBoxService = this.View.GetService<IDisplayMessage>();
messageBoxService.ShowMessage("Welcome to a small Onyx Demo.\r\n\r\n " +
"Please pick the Disciples.xml file from the debug directory","Onyx demo",
MessageBoxButton.OK,MessageBoxImage.Information);
简单而干净。
常用对话框(服务)
显示常用对话框,如OpenFileDialog/SaveFileDialog等,是另一个可能导致问题的实际问题,因为这些通常在代码隐藏中完成,而这是我们试图避免的。
同样,Onyx轻松地处理了这些。让我们看看Onyx是如何处理的。在Onyx中,有一个名为UIElementServices
的类,用于提供UI服务(我们之前提到过);还记得吗,它看起来是这样的:
internal class UIElementServices : ServicesBase, ICommandTarget,
IFocusSuggested, IDisplayMessage, ICommonDialogProvider
可以看出,UIElementServices
类实现的接口之一是ICommonDialogProvider
,它看起来像这样:
namespace Onyx.Windows
{
/// <summary>
/// Provides factory methods for creating common dialogs.
/// </summary>
public interface ICommonDialogProvider
{
/// <summary>
/// Creates the open file dialog.
/// </summary>
/// <returns>
/// A newly created <see cref="IOpenFileDialog"/> instance.
/// </returns>
IOpenFileDialog CreateOpenFileDialog();
/// <summary>
/// Creates the save file dialog.
/// </summary>
/// <returns>
/// A newly created <see cref="ISaveFileDialog"/> instance.
/// </returns>
ISaveFileDialog CreateSaveFileDialog();
}
}
因此,可以看出Onyx目前支持OpenFileDialog
和SaveFileDialog
。我们如何从ViewModel中使用OpenFileDialog
和SaveFileDialog
?嗯,这就像这样简单:
var openFileDialogService =
this.View.GetService<ICommonDialogProvider>().CreateOpenFileDialog();
openFileDialogService.InitialDirectory =
System.IO.Directory.GetCurrentDirectory();
openFileDialogService.Filter = "XML files (*.xml)|*.xml;|All files (*.*)|*.*";
if (openFileDialogService.ShowDialog() == true)
{
string fileName = openFileDialogService.FileName;
....
....
....
....
}
INotifyPropertyChanged的优点
典型的ViewModel将包含视图将绑定的属性。为了让视图接收绑定值中的更改,ViewModel通常会实现INotifyPropertyChanged
接口(实现后,该接口会将属性更改通知给绑定的控件),该接口通常实现如下:
// This class implements a simple customer type
// that implements the IPropertyChange interface.
public class DemoCustomer : INotifyPropertyChanged
{
private string customerName = String.Empty;
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
// The constructor is private to enforce the factory pattern.
private DemoCustomer()
{
companyNameValue = "no data";
}
// This is the public factory method.
public static DemoCustomer CreateNewCustomer()
{
return new DemoCustomer();
}
public string CompanyName
{
get {return this.companyNameValue;}
set
{
if (value != this.companyNameValue)
{
this.companyNameValue = value;
NotifyPropertyChanged("CompanyName");
}
}
}
}
其中绑定更改通过使用INotifyPropertyChanged
接口的NotifyPropertyChanged(String propertyName)
方法进行通知。标准实现的问题在于它在代码中使用了魔术字符串。现在,在重构时可能会遗漏这些魔术字符串,或者魔术字符串的大小写不正确,或者拼写错误,这将导致关联的绑定对象错过绑定对象属性更改的通知。
最近,许多人一直在研究创建静态反射查找,这通常使用LINQ表达式树来完成。Onyx也不例外;它也使用LINQ表达式树来辅助INotifyPropertyChanged
接口。这意味着没有魔术字符串。让我们看一个例子:
public List<discipleinfo> DiscipleInfos
{
get { return discipleInfos; }
set
{
this.discipleInfos = value;
OnPropertyChanged(() => DiscipleInfos);
}
}
你能看到我们在使用LINQ表达式(如果你深入研究Onyx,你会发现它被声明为Expression<Func<T>>
表达式)吗?Onyx内部会这样做。如前所述,Onyx使用LINQ表达式树,并对其进行一些静态反射,以找出构成表达式树一部分的属性,然后触发常规的INotifyPropertyChanged
接口NotifyPropertyChanged(String propertyName)
方法。
/// <summary>
/// Called when a property changes, using static reflection to determine the property.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="expression">Expression that accesses the property that changed.</param>
protected void OnPropertyChanged<T>(Expression<Func<T>> expression)
{
this.OnPropertyChanged(Reflect.GetProperty(expression).Name);
}
/// <summary>
/// Called when the property <paramref name="propertyName"/> has changed.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
this.VerifyPropertyName(propertyName);
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
你能在protected void OnPropertyChanged<T>(Expression<Func<T>> expression)
方法中看到Reflect.GetProperty()
方法的调用吗?让我们更详细地研究一下。如下所示:
//--------------------------------------------------------------
// <copyright file="Reflect.cs" company="William E. Kempf">
// Copyright (c) William E. Kempf.
// </copyright>
//---------------------------------------------------------------
namespace Onyx.Reflection
{
using System;
using System.Linq.Expressions;
using System.Reflection;
/// <summary>
/// Performs static reflection.
/// </summary>
public static class Reflect
{
/// <summary>
/// Gets a <see cref="MemberInfo"/> using static reflection.
/// </summary>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="MemberInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static MemberInfo GetMember(Expression<Action> expression)
{
if (expression == null)
{
throw new ArgumentNullException(
GetMember(() => expression).Name);
}
return GetMemberInfo(expression as LambdaExpression);
}
/// <summary>
/// Gets a <see cref="MemberInfo"/> using static reflection.
/// </summary>
/// <typeparam name="T">The type of the member accessed in the
/// <paramref name="expression"/>.</typeparam>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="MemberInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static MemberInfo GetMember<T>(Expression<Func<T>> expression)
{
if (expression == null)
{
throw new ArgumentNullException(
GetMember(() => expression).Name);
}
return GetMemberInfo(expression as LambdaExpression);
}
/// <summary>
/// Gets a <see cref="MethodInfo"/> using static reflection.
/// </summary>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="MethodInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static MethodInfo GetMethod(Expression<Action> expression)
{
MethodInfo method = GetMember(expression) as MethodInfo;
if (method == null)
{
throw new ArgumentException(
"Not a method call expression", GetMember(() => expression).Name);
}
return method;
}
/// <summary>
/// Gets a <see cref="PropertyInfo"/> using static reflection.
/// </summary>
/// <typeparam name="T">The type of the member accessed in the
/// <paramref name="expression"/>.</typeparam>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="PropertyInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static PropertyInfo GetProperty<T>(Expression<Func<T>> expression)
{
PropertyInfo property = GetMember(expression) as PropertyInfo;
if (property == null)
{
throw new ArgumentException(
"Not a property expression", GetMember(() => expression).Name);
}
return property;
}
/// <summary>
/// Gets a <see cref="FieldInfo"/> using static reflection.
/// </summary>
/// <typeparam name="T">The type of the member accessed in the
/// <paramref name="expression"/>.</typeparam>
/// <param name="expression">An expression that uses member access.</param>
/// <returns>
/// A <see cref="FieldInfo"/> instance for the member accessed in the
/// <paramref name="expression"/>.
/// </returns>
public static FieldInfo GetField<T>(Expression<Func<T>> expression)
{
FieldInfo field = GetMember(expression) as FieldInfo;
if (field == null)
{
throw new ArgumentException(
"Not a field expression", GetMember(() => expression).Name);
}
return field;
}
internal static MemberInfo GetMemberInfo(LambdaExpression lambda)
{
if (lambda == null)
{
throw new ArgumentNullException(
GetMember(() => lambda).Name);
}
MemberExpression memberExpression = null;
if (lambda.Body.NodeType == ExpressionType.Convert)
{
memberExpression =
((UnaryExpression)lambda.Body).Operand as MemberExpression;
}
else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpression = lambda.Body as MemberExpression;
}
else if (lambda.Body.NodeType == ExpressionType.Call)
{
return ((MethodCallExpression)lambda.Body).Method;
}
if (memberExpression == null)
{
throw new ArgumentException(
"Not a member access", GetMember(() => lambda).Name);
}
return memberExpression.Member;
}
}
}
Bill在他的博客上对此进行了更详细的介绍:反思代码:静态反射。这很流行。
监听典型的视图方法
Onyx提供的一个非常好的特性是它公开了一些很好的方法(这些方法是关联视图引发自己的事件的结果),这些方法通常会在典型的Window上找到,例如:
protected virtual void OnViewInitialized()
protected virtual void OnViewUnloaded()
protected virtual void OnViewLoaded()
Onyx通过以下方式创建这些方法:
protected ViewModel(View view)
{
this.view = view;
IElementLifetime lifetime = this.view.GetService<IElementLifetime>();
if (lifetime != null)
{
lifetime.Initialized += (s, e) => this.OnViewInitialized();
lifetime.Unloaded += (s, e) => this.OnViewUnloaded();
lifetime.Loaded += (s, e) => this.OnViewLoaded();
}
}
IElementLifetime
只是Onyx用来进一步抽象视图的接口,使其只包含这三个方法。
那么,如果你在ViewModel中工作,这对你意味着什么?嗯,这意味着我们可以简单地重写这些方法,并在这些事件发生在ViewModel所抽象的视图中时,在ViewModel中执行操作。
这是一个示例,演示代码中使用了:
/// <summary>
/// Occurs when the associated View is loaded
/// </summary>
protected override void OnViewLoaded()
{
base.OnViewLoaded();
//run messagebox on Background give view some time for view to render properly
base.View.ViewElement.Dispatcher.BeginInvoke((Action)delegate
{
var messageBoxService = this.View.GetService<IDisplayMessage>();
messageBoxService.ShowMessage("Welcome to a small Onyx Demo.\r\n\r\n " +
"Please pick the Disciples.xml file from the debug directory",
"Onyx demo", MessageBoxButton.OK,MessageBoxImage.Information);
}, DispatcherPriority.Background);
}
在这段代码中,我使用Onyx来定位一个名为IDisplayMessage
的服务,该服务可用于通过关联视图向用户显示消息框。另外值得注意的是,我正在使用与视图关联的Dispatcher
,它在Onyx中也非常容易获得;你只需使用base.View.ViewElement.Dispatcher
来获取正确视图的Dispatcher
。
Onyx的未来工作
我曾向Bill提到,我认为缺少一些东西,那就是从ViewModel打开新的实际WPF窗口的能力,以及整个导航问题。Bill知道这一点,但这是一个复杂的主题;但是,Bill提到这些功能将在未来的Onyx中出现。所以,我能说的就是请继续关注并查看Onyx CodePlex站点。
就是这样,各位
我想说的是,如果你正在进行WPF开发,那么研究Onyx绝对是值得的。正如我所说,它仍在开发中,但它是一个不错的开发,我发现它非常直观,而且它确实处理了大多数原本会令人头疼的棘手问题。Bill做得好,它真的很棒。如果我目前在工作项目中的WPF项目没有进展得那么远,我肯定会使用这个框架,并且肯定会在未来的项目中使用它。
哦,还有一件事,最近,WPF Disciples一直在就社区新的CodePlex站点进行热烈讨论。该站点位于此处。
该站点将致力于涵盖以下内容:
M-V-VM参考应用程序
项目描述
社区创建的参考应用程序,供M-V-VM框架用于演示目的,概念上类似于Web框架的Pet Shop。
此项目不会提供关于遵循Model-View-ViewModel模式的**任何**解决方案。
它将提供一个示例应用程序,该应用程序将执行在遵循M-V-VM架构时通常会导致问题的“典型”操作。然后,有了这个参考应用程序和支持文档,就可以使用特定的M-V-VM库来实现其他功能。
其他实现将不属于此项目,尽管我们会提供链接。
相反,它们将由特定M-V-VM库的作者或感兴趣的第三方提供的参考实现。这使得库开发人员更容易,因为他们不必构思自己的参考实现示例。它使最终用户受益,因为他们可以比较使用“竞争”库的各种实现,并且可以学习如何使用给定库的“最佳实践”。同样,如果你熟悉,这就相当于WPF和Silverlight的“Pet Shop”。
去看看吧。