一个真实的 MVVM Light 示例





5.00/5 (18投票s)
这是一个使用 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屏幕,展示了允许用户查看和更改由后端指定的想法。