65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF:翻转瓷砖 3D

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (57投票s)

2009年4月15日

CPOL

13分钟阅读

viewsIcon

171076

downloadIcon

3093

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控件向你展示他们的博客。

所以这里有六张截图

步骤1:点击顶部的“主按钮”

步骤2:在下载的代码所在的DEBUG文件夹中选择Disciples.xml文件

步骤3:享受3D动画

步骤4:选择一个人

步骤5:等待带有选定人物博客的幻灯片滑入

步骤6:如果你愿意,可以阅读他们的博客

我非常清楚这是一个相当无用的应用程序,我对此没有丝毫幻想。演示应用程序不是重点,它只是一个简单的演示,而真正重要的是实际的Onyx框架,所以这篇文章实际上是关于这个的。我认为我的工作是传播一些能帮助你的东西,我认为Onyx框架会帮助你。Bill做得好。好了,现在我们开始写文章的重点内容,好吗?

必备组件

运行此代码真正需要的是Onyx DLL和.NET 3.5 SP1,这些都包含在下载的代码中,但如果你想深入了解它的内部结构,你还需要Bill的另一个开源项目SpecificityOnyx使用它进行测试。这个也值得一看。我很少被某个人的代码所折服,但我真的很喜欢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目前支持OpenFileDialogSaveFileDialog。我们如何从ViewModel中使用OpenFileDialogSaveFileDialog?嗯,这就像这样简单:

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”。

去看看吧。

有用链接

© . All rights reserved.