首页 文章

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

提问于
浏览
22

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

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

Main Window

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

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

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

Questions

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

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

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

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

XAML for the MainWindow

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

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<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
    }
}

1 回答

  • 27

    神圣的长问题,蝙蝠侠!

    Q1: 该过程可行,但我不知道如何使用 LoginModel 与_1174115进行对话 .

    你可以尝试像 LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView 这样的东西

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

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

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

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

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

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

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

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


    Edit for questions in comments

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

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

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

    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 .

    当你提及其他人时,人们如何解决这个限制,单身人士是坏人

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


    Edit 2:

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

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

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

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

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

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

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

    如果将它们放在PageManager中的 ObservableCollection<ViewModelBase> 属性中,则只需要创建MainWindow 's TabControl so that it'的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类型创建默认页面 . 我还有一些从ViewModelBase继承的ViewModel基类,它们提供了页面类型之间的职责分离 .

    • 例如,SearchablePage类将仅显示特定类型系统中所有对象的列表,并提供“添加”,“编辑”,“刷新”和“筛选”命令 .

    • MaintenancePage将从数据库中检索完整对象,动态生成和定位对象公开的字段的控件,根据对象拥有的任何集合创建子页面,并提供要使用的保存和删除命令 .

    • 如果我们仍然没有要使用的模板,请抛出错误,以便开发人员知道出现了问题 .

    • 在catch块中,发生的任何运行时错误都会在友好的ErrorPage中显示给用户 .

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

相关问题