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

一个真实的 MVVM Light 示例

starIconstarIconstarIconstarIconstarIcon

5.00/5 (18投票s)

2017年3月1日

CPOL

15分钟阅读

viewsIcon

48601

downloadIcon

2873

这是一个使用 MVVM Light 实现项目的更全面的示例

引言

我曾参与过几个使用MVVMLight的项目,但从未从头开始过。使用一项技术来开发一个项目,然后又必须从零开始,这感觉非常不同。所以我开始寻找文档和示例,但很少,而且没有一个特别全面。

背景

我正在进行的项目是一个仪器的前端,所以它不像大多数数据库驱动的业务应用程序。不仅如此,他们还希望能够容纳不同的仪器。这意味着前端应尽可能少地对屏幕应该是什么样子做出假设。支持这种设计的一部分是使用ViewModel知道它需要的Model的概念。它通过使用SimpleIoc获取Model来实现。这样,后端就可以提供正确的仪器Model。另外,后端负责使用SimpleIoc注册正确的接口类。另一个类控制导航。这样,如果仪器需要一个非常不同的屏幕树,那么后端只需要实例化正确版本的IDisplayController并通过SimpleIoc进行注册即可。

概述

和许多其他人一样,当我开始新的WPF项目时,我在基础架构方面遇到了问题,因为Microsoft在Visual Studio中提供的工具。在创建WPF项目时,我发现我需要支持像RelayCommand或DelegateCommand(Prism)这样简单的东西,以及一个用于INotifyPropertyChanged的基 ViewModel。在以前从头开始的项目中,我只是创建了自己需要的辅助类。MVVM Light还提供其他功能,有助于创建更易于维护的项目。我创建的项目带有假设,但随着项目的演变,它们变得更加复杂,因此更难维护。我再次从头开始,在听完需求并结合我过去的经验后,决定这次真的应该使用一个框架:通常有两个框架被使用:MVVM Light 和 Prism。我两者都用过,并决定Prism更重量级,而MVVM Light足以满足应用程序的需求。

我正在构建的应用程序不像大多数应用程序,因为它是一个仪器的前端,而且他们希望有很多灵活性,因为仪器可能不同,需要不同的屏幕树。我显然会使用RelayCommand和ViewModelBase,但我还希望大量依赖SimpleIoc。

由于这些需求,顶层设计可能与我创建数据库驱动的东西非常不同。因此,它可能不是其他人愿意用来创建应用程序的模板。它也只演示了MVVM Light的一些功能。我在本项目中使用以下内容:

  • ViewModelBase
  • SimpleIoc
  • DIspatchHelper

MVVM Light提供的另一个有用的功能是Messenger。我以前用过Messenger,并且我很喜欢Messenger或EventAggregator(Prism)提供的解耦能力,但我确信它们会带来性能损失,并且对于新人来说也更难上手。我考虑过在某些地方使用Messenger,但能够用简单的事件来替换它。如果我想到Messenger有意义的地方,我会将其包含在示例中。

项目

此应用程序分为多个项目,目的是封装功能并减少依赖关系。

Interfaces:包含接口和一些类,用于支持后端和UI之间的通信。

ApplicationResources:包含UI使用的图形元素,如图标和jpeg图像。

Backend:这模拟了中间件的代码。

CommonUI:可用于其他项目的代码,如控件、转换器和行为。

Startup:初始化应用程序并将UI与后端隔离的项目。

UI:包含MainWindow、UserControl视图和ViewModel以及模拟器,以便在设计模式下给UserControl提供显示内容。

启动

解决方案中有一个项目仅用于启动。原因是这样UI项目和后端项目就不需要互相引用。启动项目的唯一职责是初始化UI和后端,并协调初始化。

UI和后端都有Startup类来初始化前端和后端。两个类都有一个Initialize方法来实际启动进程。后端有一个Initialized事件,用于指示Startup项目前端初始所需的Models。当Startup项目准备好并订阅了Initialized事件后,它会调用后端Initialize方法。当观察到Initialized事件时,Startup调用前端Initialize方法。

public partial class App : Application
{
   Controller.StartUp _controllerStartUp; protected override void OnStartup(StartupEventArgs e)
   {
      base.OnStartup(e);
      _controllerStartUp = new Controller.StartUp();
      _controllerStartUp.Initialized += (sender, args) =>
      {
         UI.Startup.Initialize();
      };

      _controllerStartUp.Initialize();
   }
}

可以看出,Startup项目需要了解的前端和后端就是如何初始化它们。

接口

该项目主要由接口组成,但也包含一些用于前后端之间通信的类。每种类型都有一个文件夹。

ViewModel Interfaces:每个ViewModel的接口实际上是空的,因为它被后端用来告诉UI项目显示哪个屏幕。UI项目使用此接口来查找关联的ViewModel。这意味着每个接口只有一个ViewModel派生。后端负责屏幕之间的导航。所有ViewModel的类都在UI项目中。

Model Interfaces:ViewModel依赖于后端的数据。ViewModel使用SimpleIoc查找所需数据的Model接口。ViewModel知道它们需要的数据,并且用Model接口来表示。通常,Model类是后端的一部分…后端必须在请求显示需要Model的屏幕之前实例化并注册这些Model类。

Interfaces项目中实际上定义了一个Model类,用于按钮,包括按钮文本和按下按钮时执行的方法。这是一个简单的类,用于传递信息,不需要特殊逻辑。

Event Argument Classes:Models中有许多事件会通知ViewModel发生了某种需要执行的操作,反之亦然。事件参数类必须同时被Model(通常定义在后端)和ViewModel(定义在UI项目中)所知。

Enums:UI项目和后端都使用一些枚举。

ClassLocator:ClassLocator是一个包装SimpleIoc的类。ServiceLocator通常与SimpleIoc一起使用,但ServiceLocator类被Microsoft视为过时。为了消除后端项目需要引用MVVM Light项目的需要,这个ClassLocator类提供了一个接口供任何需要的方法使用。由于这个类用于所有定位器服务,因此可以从默认的SimpleIoc进行更改。

public static class ClassLocator
{
   public static ISimpleIoc Instance => SimpleIoc.Default; public static TClass GetInstance<TClass>() 
         where TClass : class => Instance.GetInstance<TClass>(); 
   public static object GetInstance(Type type) => Instance.GetInstance(type);

   public static void Register<TClass>(Func<TClass> action) 
         where TClass : class => Instance.Register<TClass>(action) ;

}

如果您熟悉ClassLocator,您会注意到并非所有方法都已暴露,这些只是解决方案中使用的那些。

DisplayController:DisplayController类是用于控制显示哪个屏幕的类。它必须立即可用,以便在后端准备好时更改显示。因此,后端不负责创建和注册此类,而是在应用程序初始化时完成。

public class DisplayController : IDisplayController
{
   public event EventHandler<ChangeDisplayEventArgs> ChangeDisplayEvent; 

   public void ChangeDisplay(Type viewModelInterface, bool createNew = false)
   {
      DispatcherHelper.CheckBeginInvokeOnUI(()=>
      ChangeDisplayEvent?.Invoke(this, new ChangeDisplayEventArgs(viewModelInterface, createNew)));
   } 

   public void ChangeDisplay(Type viewModelInterface, DisplayActionTypes displayAction,
         bool createNew = false)
   {
      DispatcherHelper.CheckBeginInvokeOnUI(() =>
         ChangeDisplayEvent?.Invoke(this,
            new ChangeDisplayEventArgs(viewModelInterface, displayAction, createNew)));
   } 

   public void RevertDisplay()
   {
      DispatcherHelper.CheckBeginInvokeOnUI(()=>
            ChangeDisplayEvent?.Invoke(this, new ChangeDisplayEventArgs()));
   }
}

有一个事件,MainViewModel会订阅该事件以获取显示新ViewModel的通知,以及后端可以用来更改显示屏幕的若干方法。MainViewModel和后端都可以使用Interfaces项目中的SimpleIoc找到此类的实例。

该类维护一个堆栈,可以在替换ViewModel时将其推入其中,以便轻松返回到之前的显示,而无需知道之前的显示是什么。目前有三种方法可以更改屏幕:恢复到之前的屏幕,使用默认显示操作更改显示(将当前显示推送到堆栈并替换它),以及允许指定对当前显示的处理方法(包括将其替换为堆栈顶部的显示)。还有一个参数用于指定ViewModel是新创建的,这意味着当UI完成使用后,它将被销毁(这只会在显示未推送到堆栈时发生)。很多功能都在ChangeDisplayEvent的参数类中。

public class ChangeDisplayEventArgs
{
   public static readonly Dictionary<Type, Type> InterfaceTypeDictionary
      = new Dictionary<Type, Type>(); 

   public ChangeDisplayEventArgs(Type displayViewModelInterface, bool createNew = false) :
         this(displayViewModelInterface, DisplayActionTypes.Replace, createNew) { } 

   public ChangeDisplayEventArgs(Type viewModelInterface, DisplayActionTypes displayAction,
         createNew = false)
   {
      ViewModelInterface = viewModelInterface;
      Debug.Assert(displayAction != DisplayActionTypes.PopPreviousDisplay || viewModelInterface == null,
            "PopPreviousDisplay requested with a specification of DisplayViewModelInterface");
      DisplayActionType = displayAction;
      CreateNew = createNew;
   } 

   /// <summary>
   /// This is used to just pop the previous display
   /// </summary>
   public ChangeDisplayEventArgs()
   {
      DisplayActionType = DisplayActionTypes.PopPreviousDisplay;
   } 

   public Type ViewModelInterface { get; } 
   public DisplayActionTypes DisplayActionType { get; } 
   public bool CreateNew { get; }
}

后端

后端需要初始化和注册驱动UI的Models。它还使用DisplayController类的实例来更改用户看到的屏幕。我本可以使用Messenger进行通信,但Messenger这类东西有很多开销,而且据我所理解,应该尽量少用。因此,事件被大量用于与UI通信。

带命令事件处理程序更改显示的Model的简单示例

当用户执行诸如按钮单击之类的操作时要执行的代码使用EventHandler<CommandArgs>。目前CommandArgs相当简单,只有一个可选参数,这意味着如果一个Command包含一个CommandParameter,它可以被传递给执行代码。

public class CommandArgs
{
   public CommandArgs(object commandParameter = null)
   {
       CommandParameter = commandParameter;
   }

   public object CommandParameter { get; }
}

AboutModel

Model需要处理命令的一个简单示例是AboutModel。

public class AboutModel : IAboutModel
{
   private readonly IDisplayController _displayController; public AboutModel()
   {
      _displayController = ClassLocator.GetInstance<IDisplayController>();
      Debug.Assert(_displayController != null,
            IDisplayController not found for AboutModel");
   } 
   
   public void AboutOkCommand(object sender, CommandArgs a)
   {
      _displayController.RevertDisplay();
   }
}

AboutModel继承自IAboutModel,这样使用它的ViewModel就可以使用ClassLocator在In中找到实例。在这种情况下,Model需要使用ClassLcator获取IDisplayController的实例,以便更改显示。这可以在构造函数中看到。AboutOKCommand遵循EventHandler<CommandArgs>接口。它使用DisplayControl的RevertDisplay方法从其ViewModel堆栈中弹出之前的显示ViewModel,并将其用于活动ViewModel的绑定。

BottomBarMenu:强制UI更新

BottomBarModel控制MainWindow右下角的菜单按钮和左侧的消息。

public class BottomBarModel : IBottomBarModel
{
   private readonly IDisplayController _displayController; public BottomBarModel()
   {
      _displayController = ClassLocator.GetInstance<IDisplayController>();
      Debug.Assert(_displayController != null, "IDisplayController not found for BottomBarModel");
      UserMessage = "Initializing...";
      MessageType = MessageTypeEnum.Information;
   } 

   public void MenuCommand(object sender, CommandArgs a)
   {
      _displayController.ChangeDisplay(typeof(IMenuViewModel),
      DisplayActionTypes.PushPreviousDisplay, true);
   } 

   public void UpdateUserMessage(string message, MessageTypeEnum messageType)
   {
      UpdateUserMessageEvent?.Invoke(this,
             new UpdateUserMessageEventArgs(message, messageType));
      UserMessage = message;
      MessageType = messageType;
   } 

   public event EventHandler<UpdateUserMessageEventArgs> UpdateUserMessageEvent;

   public string UserMessage { get; private set; }

   public MessageTypeEnum MessageType { get; private set; }
}

它还有一个方法来处理“Menu”按钮的按下,因此此类也需要DisplayController的实例。在这种情况下,该方法使用ChangeDisplay与IMenuViewModel类型,并将bool设置为true以创建IMenuViewModel派生类的实例。

后端通过调用UpdateUserMessage来更改显示给用户的消息,该消息有消息和消息类型(将更改背景)的参数。这会更新属性并触发UI的UpdateEvent。

注意:我可以合并DisplayController和BottomBarModel,但我认为将它们封装在不同的接口中更好。它们仍然可以合并。

MenuModel

MenuModel,继承自IMenuModel,是一个有趣的案例,因为它允许将按钮信息列表传递给UI,其中当前包括按钮的标签以及按下按钮时执行的操作。

public class MenuModel : IMenuModel
{
   private readonly IDisplayController _displayController; public MenuModel()
   {
      _displayController = ClassLocator.GetInstance<IDisplayController>();
      Debug.Assert(_displayController != null, 
            "IDisplayController not found for MenuModel"); 
      MenuButtonCommands = new List<IButtonModel>()
      {
         new MenuButtonModel ("Home", (s, a) =>
         {
            _displayController.RevertDisplay();
         }),

         new MenuButtonModel ("About", (s, a) =>
         {
            _displayController.ChangeDisplay(typeof(IAboutViewModel),
                DisplayActionTypes.PushPreviousDisplay, true);
         })
      };
   } 

   public IList<IButtonModel> MenuButtonCommands { get;}
}

这通常是计划用于与所有屏幕交互的模式,以便后端可以指定屏幕的内容。

UI项目

UI项目主要由视图和ViewModel组成,但也包含Model的模拟器,这些模拟器确保在创建UserControl文件的设计视图时没有错误,并用于提供允许在UserControl设计视图中看到示例设计元素的属性。还有一个关键元素是查找UI项目中实现后端用于告诉UI显示哪个屏幕的接口的类的扩展方法。

public static BasicViewModelBase ViewModel(this ChangeDisplayEventArgs changeDisplayEventArgs)
{
   BasicViewModelBase viewModel;
   var viewModelInterface = changeDisplayEventArgs.ViewModelInterface;
   var dictionary = ChangeDisplayEventArgs.InterfaceTypeDictionary; 

   if (changeDisplayEventArgs.CreateNew)
   {
      if (dictionary.ContainsKey(viewModelInterface))
      {
         viewModel = (BasicViewModelBase)Activator
               .CreateInstance(dictionary[changeDisplayEventArgs.ViewModelInterface]);
         viewModel.DisposeOfAfterUse = changeDisplayEventArgs.CreateNew;
      }
      else
      {
         var names = Assembly.GetExecutingAssembly().GetTypes().Select(i => i.Name); 
         var displayViewModelClass = Assembly.GetExecutingAssembly().GetTypes()
            .FirstOrDefault(i => i.IsClass && i.GetInterfaces().Contains(viewModelInterface));
         Debug.Assert(displayViewModelClass != null,
            $"Could not find a Type that is derived from the interface {viewModelInterface.Name}");
         viewModel = (BasicViewModelBase)Activator.CreateInstance(displayViewModelClass);

         ChangeDisplayEventArgs.InterfaceTypeDictionary
               .Add(viewModelInterface, displayViewModelClass);
         viewModel.DisposeOfAfterUse = changeDisplayEventArgs.CreateNew;
      }
   }
   else
   {
      viewModel = (BasicViewModelBase)
      ClassLocator.GetInstance(changeDisplayEventArgs.ViewModelInterface);
   }
   return viewModel;
}

对于ChangeDisplayEventArgs使用扩展方法,因为只有当ViewModel类搜索以查找实现用于指定屏幕的接口的类时,此代码才有效。这意味着此设计仅适用于所有ViewModel都在同一程序集中的小型应用程序。可以编写代码来搜索多个程序集。

目前ClassLocator类中存在一些未经测试的代码。ClassLocator类添加了几个SimpleIoc中不存在的方法:第一个是检查实例是否存在,SimpleIoc代码用于检查类型的实例是否存在——SimpleIoc只有一个通用的检查方法。第二个是注册一个实例,将查找作为类型而不是泛型传递。

为了提高性能,一旦找到与指定接口匹配的类,该关联就会保存在静态字典中。如果找不到关联,则使用反射来查找程序集中的类,并搜索继承自该接口的类。

ViewModels

所有ViewModel都继承自UiViewModelBase,而UiViewModelBase继承自MVVM Light的ViewModelBase。我本可以使用一个接口,但这样就无需同时引用ViewModelBase和接口。我使用ViewModelBase的原因是它支持INotifyPropertyChanged,并重写了CleanUp方法。UiViewModelBase目前有一个DisposeOfAfterUse属性,用于指示此ViewModel在使用完当前实例后将被丢弃。

AboutViewModel

以下是AboutVIewModel使用的类。

public class AboutViewModel : UiViewModelBase, IAboutViewModel
{
   private IAboutModel _aboutModel; public AboutViewModel()
   {
      if (this.IsInDesignMode)
      {
         _aboutModel = new AboutModelSimulator();
      }
      else
      {
         _aboutModel = ClassLocator.GetInstance<IAboutModel>();
         Debug.Assert(_aboutModel != null, " IAboutModel not found for AboutViewModel");
      }
   } 

   public string Version =>  System.Reflection.Assembly.GetExecutingAssembly()
      .GetName().Version.ToString(); 

   public string Copyright => GeneralHelpers.GetCopyright(); 

   public RelayCommand OkCommand => new RelayCommand(() =>
         _aboutModel.AboutOkCommand(this, new CommandArgs())); 

   public override void Cleanup()
   {
      _aboutModel = null;
      base.Cleanup();
   }
}

该类有两个用于显示数据的属性(Version和Copyright),以及一个用于ICommand接口的属性。由于Version和Copyright不会改变,因此无需为这两个属性实现INotifyPropertyChanged。OkCommand属性访问AboutModel的AboutOkCommand方法。无需担心Model中的更改,因为这发生在调用方法时。

MainViewModel

主窗口的ViewModel如下。

public class MainViewModel : UiViewModelBase
{
   private readonly IDisplayController _displaController;
   private readonly Stack<UiViewModelBase> _primaryDisplayStack
            = new Stack<UiViewModelBase>(); private UiViewModelBase _PrimaryViewModel;

   private string _userMessage;

   private IBottomBarModel _bottomBarModel;

   private MessageTypeEnum _messageType; public MainViewModel()
   {
      _displayController = ClassLocator.Instance.GetInstance<IDisplayController>();
      Debug.Assert(_displayController != null, nameof(IDisplayController)
            + " not found for " + GetType().Name);
      _displayController.ChangeDisplayEvent += DisplayChangeEventHandler;
      PrimaryViewModel = new SplashScreenViewModel { DisposeOfAfterUse = true }; 
      if (this.IsInDesignMode)
      {
         _bottomBarModel = new BottomBarModelSimulator();
      }
      else
      {
         _bottomBarModel = ClassLocator.GetInstance<IBottomBarModel>();
         Debug.Assert(_bottomBarModel != null, "IBottomBarModel not found for MainViewModel");
         _bottomBarModel.UpdateEvent += UpdateUserMessageEventHandler;
      }

      UserMessage = _bottomBarModel.UserMessage;

      MessageType = _bottomBarModel.MessageType;

   } 

   private void UpdateUserMessageEventHandler(object sender, UpdateEventArgs e)
   {
      UserMessage = _bottomBarModel.UserMessage;
      MessageType = _bottomBarModel.MessageType;
   } 

   private void DisplayChangeEventHandler(object sender, ChangeDisplayEventArgs changeDisplayEventArgs)
   {
      //Should not be keeping (or using) previous display and disposing
      switch (changeDisplayEventArgs.DisplayActionType)
      {
         case DisplayActionTypes.PopPreviousDisplay:
            PrimaryViewModel = _primaryDisplayStack.Pop();
            return;
         case DisplayActionTypes.PushPreviousDisplay:
            _primaryDisplayStack.Push(PrimaryViewModel);
            break;
      } 
      if (PrimaryViewModel.DisposeOfAfterUse &&
               !(changeDisplayEventArgs.DisplayActionType == DisplayActionTypes.PushPreviousDisplay))
         PrimaryViewModel.Cleanup();
      PrimaryViewModel = changeDisplayEventArgs.ViewModel();
   } 

   public UiViewModelBase PrimaryViewModel
   {
      get { return _PrimaryViewModel; }
      set { Set(ref _PrimaryViewModel, value); }
   } 

   public string UserMessage
   {
      get { return _userMessage; }
      set { Set(ref _userMessage, value); }
   } 

   public MessageTypeEnum MessageType
   {
      get { return _messageType; }
      set { Set(ref _messageType, value); }
   } 

   public RelayCommand MenuCommand => new RelayCommand(() =>
            _bottomBarModel.MenuCommand(this, new CommandArgs())); 

   public override void Cleanup()
   {
      _displayController.ChangeDisplayEvent -= DisplayChangeEventHandler;
      _bottomBarModel.UpdateEvent -= UpdateUserMessageEventHandler;
      while (_primaryDisplayStack.Count > 0)
         _primaryDisplayStack.Pop().Cleanup();
   }
}

此ViewModel有几个功能。

  • 监视IDisplayController的ChangeDisplayEvent,以获取PrimaryViewModel属性的更新,该属性会更改MainWindow主区域使用的视图。
  • 监视IBottomBarModel的Update事件,以获取MainWindow左下角消息显示的用户消息和消息类型的更新。当IAboutWindow的Update事件触发时,UserMessage和MessageType属性会被更新。
  • 为MainWindow右下角的Menu按钮提供一个ICommand属性。当执行ICommand时,将执行IBottomBarModel的MenuCommand。

构造函数使用Interfaces项目中的ClassLocator获取IDisplayController和IBottomBarModel的实例,并订阅这些接口的事件。还有一个测试ViewModel是否处于设计模式的测试,使用MVVM Light的IsInDesignMode属性。如果处于设计模式,则使用IBottomBarModel模拟器而不是通过ClassLocator使用实例。

还有一个从MVVM Light的ViewModelBase继承的CleanUp虚拟方法。在这段代码中可以看到,它用于取消订阅构造函数中订阅的事件,并删除任何引用。

ViewModel构造函数和CleanUp方法的函数对于此设计中的大多数ViewModel来说将非常相似。

当任何属性更新时,MVVM Light的Set方法用于执行INotifyPropertyChanged的PropertyChangedEvent。

视图

有一个派生自Window的MainWindow,以及UserControls的视图。

MainWindow

主窗口有一个ContentPresenter控件,其内容绑定到DataContext的PrimaryViewModel属性。此属性的对象类型决定了显示哪个UserControl。

<Window x:Class="UI.MainWindow"
        xmlns="<a href="http://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
        xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">http://schemas.microsoft.com/winfx/2006/xaml</a>"
        xmlns:converters="clr-namespace:Interfaces.Assets.Converters;assembly=Interfaces"
        xmlns:d="<a href="http://schemas.microsoft.com/expression/blend/2008">http://schemas.microsoft.com/expression/blend/2008</a>"
        xmlns:mc="<a href="http://schemas.openxmlformats.org/markup-compatibility/2006">http://schemas.openxmlformats.org/markup-compatibility/2006</a>"
        xmlns:view="clr-namespace:UI.View"
        xmlns:viewModel="clr-namespace:UI.ViewModel"
        Title="MainWindow"
        MinWidth="800"
        MinHeight="600"
        mc:Ignorable="d">
    <Window.DataContext>
        <viewModel:MainViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="8*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto"
                           MinHeight="40" />
        </Grid.RowDefinitions>
        <ContentPresenter Grid.ColumnSpan="2"
                          Content="{Binding PrimaryViewModel}" />
        <Border Grid.Row="1"
                Grid.Column="0"
                Background="{Binding MessageType, 
                            Converter={converters:MessagetTypeToBackgroundBrushConverter}}"
                BorderBrush="Black"
                BorderThickness="2"
                CornerRadius="3">
            <TextBlock Margin="10,0"
                       VerticalAlignment="Center"
                       Text="{Binding UserMessage}" />
        </Border>
        <Button Grid.Row="1"
                Grid.Column="1"
                Background="LightBlue"
                Command="{Binding MenuCommand}"
                Content="Menu" />
    </Grid>
</Window>

还有用于向用户显示状态消息的控件,以及一个用于打开底部菜单UserControl的按钮。窗口还将DataContext指定为主ViewModel。

ViewModel与View的关联是通过DataTemplate完成的,该DataTemplate指定了DataType,其内容是应与该DataType关联的UserControl。

        <DataTemplate DataType="{x:Type viewModel:AboutViewModel}">
            <view:AboutView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewModel:MenuViewModel}">
            <view:MenuView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewModel:SplashScreenViewModel}">
            <view:SplashScreenView />
        </DataTemplate>

屏幕截图

初始屏幕,向用户显示“Initializing”消息,背景为绿色,表示这是信息性消息。

10秒后,消息已更新为“10 seconds have passed”,背景为黄色,表示这是警告消息。

点击右下角的“Menu”按钮后显示的屏幕。该屏幕有一组按钮,允许导航到应用程序中的其他屏幕。

点击“About”按钮后显示的屏幕。点击“OK”按钮会弹出菜单屏幕,从而返回菜单屏幕。

结论

该应用程序目前非常简单,只包含初始屏幕、菜单屏幕和关于屏幕。本应有一些额外的屏幕来显示状态,并允许查看和可能更新配置值。我计划添加一些屏幕,使其看起来更像一个功能齐全的应用程序,并分割UI项目为视图和ViewModel项目。欢迎对如何使其成为更有用的示例提出建议。请理解,我使用了MVVM Light,但并非所有功能。另外,如果有人能给我一个使用Messenger的真正充分的理由,我很想听听,因为我想包含Messenger的代码,但还没有找到一个好的理由。

历史

  • 2017年3月1日:初始版本
  • 2017年3月3日:更新了UniformGridItemsControl,并添加了新的Configuraiton屏幕,展示了允许用户查看和更改由后端指定的想法。
© . All rights reserved.