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

Jon*_*lis 22 c# wpf user-interface 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

<Window x:Class="WpfApplication1.MainWindow"
    xmlns:local="clr-namespace:WpfApplication1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="372" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <local:HeaderView Grid.ColumnSpan="2" />

        <local:ButtonsView Grid.Row="1" Margin="6,6,3,6" />

        <local:LoginView Grid.Column="1" Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</Window>
Run Code Online (Sandbox Code Playgroud)

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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<PasswordBox>(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
    }
}
Run Code Online (Sandbox Code Playgroud)

fat*_*tty 28

神圣的长问题,蝙蝠侠!

Q1: 这个过程会起作用,但我不知道如何使用它LoginModel来与之交谈MainWindowViewModel.

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

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

实现LoginCommandon或者LoginViewModelSingleton(就我个人而言,我可能会ViewModel在ViewModel和"后端"实用程序类之间添加一定程度的分离).此login命令将调用单例上的方法来执行登录.

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

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

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

问题3: 我不确定我是否理解这个,但希望你的意思是"当用户没有登录时显示登录页面",如果是这种情况,你可以指示你ViewModelToViewConverter在用户没有登录时忽略任何指令(通过检查SecurityContext单例)而只显示LoginView模板,如果您希望只有特定用户有权查看或使用的页面,您可以在构建View之前检查安全要求,并将其替换为安全提示.

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

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


编辑评论中的问题

LoginManagerSingleton如何直接与MainWindowView对话.不应该所有内容都通过MainWindowViewModel,以便MainWindowView上没有任何代码

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

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

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

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

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

我不会将PageManager设计为View-ViewModel设计,只是一个普通的家用单例实现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类,只需要那些顶级的.所有其他页面都将是其父级的子级,以便您更好地控制层次结构,并允许您逐步删除"保存"和"关闭"事件.

如果将它们放在ObservableCollection<ViewModelBase>PageManager 中的属性中,则只需要创建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;
    }
Run Code Online (Sandbox Code Playgroud)

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

  • 如果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;
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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

  • 神圣的长答案,罗宾!(...我知道蝙蝠侠不会说"圣洁".) (5认同)