WPF MVVM – 简单登录到应用程序

我正在继续学习WPF,并且目前专注于MVVM并使用Karl Shifflett的“MVVM In a Box”教程。 但是有一个关于在views / viewmodels之间共享数据以及它如何在屏幕上更新视图的问题。 ps我还没有报道过IOC。

下面是我在测试应用程序中的MainWindow的屏幕截图。 它分为3个部分(视图),一个标题,一个带按钮的滑动面板,其余部分作为应用程序的主视图。 应用程序的目的很简单,登录到应用程序。 在成功登录后,登录视图应该被新视图(即OverviewScreenView)替换,并且应用程序幻灯片上的相关按钮应该变为可见。

主窗口

我认为应用程序有2个ViewModel。 一个用于MainWindowView,一个用于LoginView,因为MainWindow不需要具有Login命令,所以我将它保持独立。

由于我尚未介绍IOC,我创建了一个LoginModel类,它是一个单例。 它只包含一个属性“public bool LoggedIn”,以及一个名为UserLoggedIn的事件。

MainWindowViewModel构造函数注册到事件UserLoggedIn。 现在在LoginView中,当用户在LoginView上单击Login时,它会在LoginViewModel上引发一个命令,如果正确输入用户名和密码,则会调用LoginModel并将LoggedIn设置为true。 这会导致UserLoggedIn事件触发,在MainWindowViewModel中处理该事件以使视图隐藏LoginView并将其替换为不同的视图,即概览屏幕。

问题

Q1。 明显的问题,就是这样登录正确使用MVVM。 即控制流程如下。 LoginView – > LoginViewViewModel – > LoginModel – > MainWindowViewModel – > MainWindowView。

Q2。 假设用户已登录,并且MainWindowViewModel已处理该事件。 您将如何创建新视图并将其放在LoginView所在的位置,同样如何在不需要时处理LoginView。 MainWindowViewModel中是否存在类似“UserControl currentControl”的属性,该属性设置为LoginView或OverviewScreenView。

Q3。 MainWindow是否应该在visual studio设计器中设置LoginView。 或者它应该留空,并以编程方式确认没有人登录,因此一旦加载了MainWindow,它就会创建一个LoginView并在屏幕上显示它。

下面的一些代码示例是否有助于回答问题

MainWindow的XAML

               

MainWindowViewModel

 using System; using System.Windows.Controls; using WpfApplication1.Infrastructure; namespace WpfApplication1 { public class MainWindowViewModel : ObservableObject { LoginModel _loginModel = LoginModel.GetInstance(); private UserControl _currentControl; public MainWindowViewModel() { _loginModel.UserLoggedIn += _loginModel_UserLoggedIn; _loginModel.UserLoggedOut += _loginModel_UserLoggedOut; } void _loginModel_UserLoggedOut(object sender, EventArgs e) { throw new NotImplementedException(); } void _loginModel_UserLoggedIn(object sender, EventArgs e) { throw new NotImplementedException(); } } } 

LoginViewViewModel

 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Controls; using System.Windows.Input; using WpfApplication1.Infrastructure; namespace WpfApplication1 { public class LoginViewViewModel : ObservableObject { #region Properties private string _username; public string Username { get { return _username; } set { _username = value; RaisePropertyChanged("Username"); } } #endregion #region Commands public ICommand LoginCommand { get { return new RelayCommand(LoginExecute, pb => CanLoginExecute()); } } #endregion //Commands #region Command Methods Boolean CanLoginExecute() { return !string.IsNullOrEmpty(_username); } void LoginExecute(PasswordBox passwordBox) { string value = passwordBox.Password; if (!CanLoginExecute()) return; if (_username == "username" && value == "password") { LoginModel.GetInstance().LoggedIn = true; } } #endregion } } 

神圣的长问题,蝙蝠侠!

Q1:该过程可行,但我不知道如何使用LoginModelMainWindowViewModel

你可以试试像LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView

我知道单身人士被认为是反模式,但我觉得这对于像这样的情况最容易。 这样,单例类可以实现INotifyPropertyChanged接口,并在检测到login \ out事件时引发事件。

LoginViewModel或Singleton上实现LoginCommand (就个人而言,我可能会在ViewModel上实现这一点,以在ViewModel和“后端”实用程序类之间添加一定程度的分离)。 此login命令将调用单例上的方法来执行登录。

Q2:在这些情况下,我通常有(又一个)单例类作为PageManagerViewModelManager 。 此类负责创建,处理和保持对顶级页面或CurrentPage的引用(仅限单页情况)。

我的ViewModelBase类还有一个属性来保存显示我的类的UserControl的当前实例,这样我就可以挂钩Loaded和Unloaded事件。 这使我能够拥有可在ViewModel定义的虚拟OnLoaded(), OnDisplayed() and OnClosed()方法,以便页面可以执行加载和卸载操作。

当MainWindowView显示ViewModelManager.CurrentPage实例时,一旦此实例发生更改,Unloaded事件将触发,我的页面的Dispose方法将被调用,最终GC进入并整理其余部分。

Q3:我不确定我是否理解这个,但希望你的意思是“当用户未登录时显示登录页面”,如果是这种情况,你可以指示你的ViewModelToViewConverter在用户未登录时忽略任何指令在(通过检查SecurityContext单例)而只显示LoginView模板,这对于您希望只有特定用户有权查看或使用的页面的情况也很有用,您可以在构建View之前检查安全要求,并替换它带有安全提示。

对不起,答案很长,希望这有帮助:)

编辑:另外,你拼错了“管理”


编辑评论中的问题

LoginManagerSingleton如何直接与MainWindowView对话。 不应该一切都通过MainWindowViewModel,以便MainWindowView上没有任何代码

对不起,澄清一下 – 我不是说LoginManager直接与MainWindowView交互(因为这应该只是一个视图),而是LoginManager只是设置一个CurrentUser属性来响应LoginCommand所做的调用,反过来引发PropertyChanged事件,MainWindowView(正在监听更改)会相应地做出反应。

然后,当您实施IOC时,LoginManager可以调用PageManager.Open(new OverviewScreen()) (或PageManager.Open("overview.screen") ),例如将用户重定向到用户登录后看到的默认屏幕。

LoginManager本质上是实际登录过程的最后一步,View只是适当地反映了这一点。

此外,在输入时,我发现不是拥有一个LoginManager单例,而是所有这些都可以放在PageManager类中。 只需要一个Login(string, string)方法,该方法设置成功登录时的CurrentUser。

我理解PageManagerView的想法,基本上是通过PageManagerViewModel

我不会将PageManager设计为View-ViewModel设计,只是实现INotifyPropertyChanged的普通INotifyPropertyChanged单例应该可以做到这一点,这样MainWindowView就可以对更改CurrentPage属性做出反应。

ViewModelBase是您创建的抽象类吗?

是。 我使用这个类作为我所有ViewModel的基类。

这个类包含

  • 所有页面上使用的属性,如Title,PageKey和OverriddenUserContext。
  • 常见的虚拟方法,如PageLoaded,PageDisplayed,PageSaved和PageClosed
  • 实现INPC并公开受保护的OnPropertyChanged方法以用于引发PropertyChanged事件
  • 并提供与页面交互的框架命令,如ClosePageCommand,SavePageCommand等。

检测到登录后,CurrentControl将设置为新视图

就个人而言,我只会持有当前正在显示的ViewModelBase的实例。 然后由ContentControl中的MainWindowView引用它,如下所示: Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}"

然后我还使用转换器将ViewModelBase实例转换为UserControl,但这纯粹是可选的; 您可以只依赖ResourceDictionary条目,但此方法还允许开发人员拦截调用并在需要时显示SecurityPage或ErrorPage。

然后,当应用程序启动时,它会检测到没有人登录,因此创建了一个LoginView并将其设置为CurrentControl。 而不是强调它默认显示LoginView

您可以设计应用程序,以便向用户显示的第一个页面是OverviewScreen的实例。 其中,由于PageManager当前具有null的CurrentUser属性,ViewModelToViewConverter将拦截此而不是显示OverviewScreenView UserControl,而是显示LoginView UserControl。

如果用户成功登录,LoginViewModel将指示PageManager重定向到原始的OverviewScreen实例,这次正确显示,因为CurrentUser属性为非null。

人们如何像其他人一样提到这个限制,单身人士是坏人

我和你在一起,我喜欢我一个好单身人士。 但是,这些的使用应限于仅在必要时使用。 但是在我看来,它们确实有完全有效的用途,但不确定是否还有其他人想在这件事情上加入?


编辑2:

您是否为MVVM使用公开可用的框架/类集

不,我使用的是我在过去12个月左右创建和改进的框架。 该框架仍然遵循大多数 MVVM准则,但包括一些个人接触,减少了编写所需的整体代码量。

例如,一些MVVM示例就像你一样建立了他们的观点; 而View在其ViewObject.DataContext属性中创建ViewModel的新实例。 这可能适用于某些人,但不允许开发人员从ViewModel挂钩某些Windows事件,如OnPageLoad()。

我的案例中的OnPageLoad()是在页面上的所有控件都已创建后调用的,并且可以在调用构造函数后的几分钟内立即进入屏幕查看,或者根本不可以。 例如,如果该页面在当前未选中的选项卡中有多个子页面,那么我可以在此处执行大部分数据加载以加快页面加载过程。

但不仅如此,通过以这种方式创建ViewModel,每个View中的代码量增加了至少三行。 这可能听起来不是很多,但是这些代码行不仅对于创建重复代码的所有视图基本相同,而且如果您的应用程序需要许多视图,则额外的行数可以非常快地加起来。 那,我真的很懒..我没有成为开发人员输入代码。

通过您对页面管理器的想法,我将在未来的修订版中做的就是像tabcontrol一样打开几个视图,其中页面管理器控制页面块而不是单个userControl。 然后,可以通过绑定到页面管理器的单独视图选择选项卡

在这种情况下,PageManager不需要直接引用每个打开的ViewModelBase类,只需要那些顶级的。 所有其他页面都将是其父级的子级,以便您更好地控制层次结构,并允许您逐步删除“保存”和“关闭”事件。

如果将它们放在PageManager中的ObservableCollection属性中,则只需要创建MainWindow的TabControl,以便它的ItemsSource属性指向PageManager上的Children属性,并让WPF引擎完成其余的工作。

你可以在ViewModelConverter上进一步扩展吗?

当然,为了给你一个大纲,显示一些代码会更容易。

  public override object Convert(object value, SimpleConverterArguments args) { if (value == null) return null; ViewModelBase vm = value as ViewModelBase; if (vm != null && vm.PageTemplate != null) return vm.PageTemplate; System.Windows.Controls.UserControl template = GetTemplateFromObject(value); if (vm != null) vm.PageTemplate = template; if (template != null) template.DataContext = value; return template; } 

通过以下部分阅读此代码,内容如下:

  • 如果value为null,则返回。 简单的空引用检查。
  • 如果该值是ViewModelBase,并且该页面已加载,则只返回该View。 如果不这样做,则每次显示页面时都会创建一个新视图,这会导致一些意外行为。
  • 获取页面模板UserControl(如下所示)
  • 设置PageTemplate属性,以便可以挂钩此实例,因此我们不会在每次传递时加载新实例。
  • 将View DataContext设置为ViewModel实例,这两行完全取代了我之前从每个视图中讨论的那三行。
  • 返回模板。 然后,它将显示在ContentPresenter中供用户查看。

     public static System.Windows.Controls.UserControl GetTemplateFromObject(object o) { System.Windows.Controls.UserControl template = null; try { ViewModelBase vm = o as ViewModelBase; if (vm != null && !vm.CanUserLoad()) return new View.Core.SystemPages.SecurityPrompt(o); Type t = convertViewModelTypeToViewType(o.GetType()); if (t != null) template = Activator.CreateInstance(t) as System.Windows.Controls.UserControl; if (template == null) { if (o is SearchablePage) template = new View.Core.Pages.Generated.ViewList(); else if (o is MaintenancePage) template = new View.Core.Pages.Generated.MaintenancePage(((MaintenancePage)o).EditingObject); } if (template == null) throw new InvalidOperationException(string.Format("Could not generate PageTemplate object for '{0}'", vm != null && !string.IsNullOrEmpty(vm.PageKey) ? vm.PageKey : o.GetType().FullName)); } catch (Exception ex) { BugReporter.ReportBug(ex); template = new View.Core.SystemPages.ErrorPage(ex); } return template; } 

这是转换器中执行大部分繁重工作的代码,通读您可以看到的部分:

  • 主try..catch块用于捕获任何类构造错误,包括,
    • 页面不存在,
    • 构造函数代码中的运行时错误,
    • 和XAML中的致命错误。
  • convertViewModelTypeToViewType()只是试图找到对应于ViewModel的View并返回它认为应该是的类型代码(这可能为null)。
  • 如果这不是null,则创建该类型的新实例。
  • 如果我们找不到要使用的View,请尝试为该ViewModel类型创建默认页面。 我还有一些从ViewModelBaseinheritance的ViewModel基类,它们提供了页面类型之间的职责分离。
    • 例如,SearchablePage类将只显示特定类型系统中所有对象的列表,并提供“添加”,“编辑”,“刷新”和“筛选”命令。
    • MaintenancePage将从数据库中检索完整对象,动态生成和定位对象公开的字段的控件,根据对象具有的任何集合创建子页面,并提供要使用的保存和删除命令。
  • 如果我们仍然没有要使用的模板,则抛出错误,以便开发人员知道出现了问题。
  • 在catch块中,发生的任何运行时错误都会在友好的ErrorPage中显示给用户。

这一切都让我专注于只创建ViewModel类,因为应用程序将简单地显示默认页面,除非View页面已由开发人员明确覆盖该ViewModel。