WPF MVVM - 简单登录到应用程序 [英] WPF MVVM - Simple login to an application

查看:77
本文介绍了WPF MVVM - 简单登录到应用程序的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm continuing to learn WPF, and focusing on MVVM at the moment and using Karl Shifflett’s "MVVM In a Box" tutorial. But have a question about sharing data between views/viewmodels and how it updates the view on the screen. p.s. I haven't covered IOC's yet.

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

Below is a screenshot of my MainWindow in a test application. Its split into 3 sections (views), a header, a slide panel with buttons, and the remainder as the main view of the application. The purpose of the application is simple, login to the application. On a successful login, the login view should disappear by it being replaced by a new view (i.e. OverviewScreenView), and relevant buttons on the slide of the application should become visible.

我认为该应用程序有 2 个 ViewModel.一个用于 MainWindowView,另一个用于 LoginView,因为 MainWindow 不需要登录命令,所以我将它分开.

I see the application as having 2 ViewModels. One for the MainWindowView and one for the LoginView, given the MainWindow doesn't need to have commands for Login so i kept it separate.

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

As i haven't covered IOC's yet, I created a LoginModel class which is a singleton. It only contains one property which is "public bool LoggedIn", and an event called UserLoggedIn.

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

The MainWindowViewModel constructor registers to the event UserLoggedIn. Now in the LoginView , when a user clicks Login on the LoginView, it raises a command on the LoginViewModel, which in turn if a username and password is correctly entered will call the LoginModel and set LoggedIn to true. This causes the UserLoggedIn event to fire, which is handled in the MainWindowViewModel to cause the view to hide the LoginView and replace it with a different view i.e. an overview screen.

问题

第一季度.明显的问题是,像这样登录是否正确使用 MVVM.即控制流程如下.LoginView --> LoginViewViewModel --> LoginModel --> MainWindowViewModel --> MainWindowView.

Q1. Obvious question, is logging in like this a correct use of MVVM. i.e. Flow of control is as follows. LoginView --> LoginViewViewModel --> LoginModel --> MainWindowViewModel --> MainWindowView.

第 2 季度.假设用户已登录,并且 MainWindowViewModel 已处理该事件.您将如何创建一个新视图并将其放在 LoginView 所在的位置,同样如何在不需要 LoginView 时处理它.MainWindowViewModel 中是否有像UserControl currentControl"这样的属性,它被设置为 LoginView 或 OverviewScreenView.

Q2. Assuming the user has logged in, and the MainWindowViewModel has handled the event. How would you go about creating a new View and putting it where the LoginView was, equally how do you go about disposing of the LoginView once it is not needed. Would there be a property in the MainWindowViewModel like "UserControl currentControl", which gets set to LoginView or a OverviewScreenView.

第三季度.MainWindow 是否应该在 Visual Studio 设计器中设置 LoginView.或者它应该留空,并以编程方式意识到没有人登录,因此一旦加载了 MainWindow,它就会创建一个 LoginView 并将其显示在屏幕上.

Q3. Should the MainWindow have a LoginView set in the visual studio designer. Or should it be left blank, and programatically it realises that no one is logged in, so once the MainWindow is loaded, then it creates a LoginView and shows it on the screen.

下面的一些代码示例,如果它有助于回答问题

Some code samples below if it helps with answering questions

主窗口的 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>

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();
        }
    }
}

登录ViewViewModel

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

推荐答案

好久不见,蝙蝠侠!

第一季度:该过程会起作用,但我不知道如何使用 LoginModelMainWindowViewModel 对话.

Q1: The process would work, I don't know about using the LoginModel to talk to the MainWindowViewModel however.

你可以尝试类似 LoginView ->登录视图模型 ->[SecurityContextSingleton ||LoginManagerSingleton] ->主窗口视图

我知道有些人认为单例模式是反模式的,但我发现这对于此类情况是最简单的.这样,单例类可以实现 INotifyPropertyChanged 接口并在检测到 login\out 事件时引发事件.

I know that singleton's are considered anti-patterns by some, but I find this to be easiest for situations like these. This way, the singleton class can implement the INotifyPropertyChanged interface and raise events whenever a login\out event is detected.

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

Implement the LoginCommand on either the LoginViewModel or the Singleton (Personally, I would probably implement this on the ViewModel to add a degree of separation between the ViewModel's and the "back-end" utility classes). This login command would call a method on the singleton to perform the login.

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

Q2: In these cases, I typically have (yet another) singleton class to act as the PageManager or ViewModelManager. This class is responsible for creating, disposing and holding references to the Top-level pages or the CurrentPage (in a single-page only situation).

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

My ViewModelBase class also has a property to hold the current instance of the UserControl that is displaying my class, this is so I can hook the Loaded and Unloaded events. This provides me the ability to have virtual OnLoaded(), OnDisplayed() and OnClosed() methods that can be defined in the ViewModel so the page can perform loading and unloading actions.

当 MainWindowView 显示 ViewModelManager.CurrentPage 实例时,一旦该实例发生变化,Unloaded 事件就会触发,我的页面的 Dispose 方法被调用,最终 GC 进入把剩下的收拾好.

As the MainWindowView is displaying the ViewModelManager.CurrentPage instance, once this instance changes, the Unloaded event fires, my page's Dispose method is called, and eventually GC comes in and tidy's up the rest.

第 3 季度:我不确定我是否理解这一点,但希望您的意思是用户未登录时显示登录页面",如果是这种情况,您可以指示您的 ViewModelToViewConverter 在以下情况下忽略任何说明用户未登录(通过检查 SecurityContext 单例)而仅显示 LoginView 模板,这在您希望只有某些用户有权查看或使用的页面的情况下也很有帮助可以在构建View之前检查安全要求,并替换为安全提示.

Q3: I'm not sure if I understand this one, but hopefully you just mean "Display login page when user not logged in", if this is the case, you could instruct your ViewModelToViewConverter to ignore any instructions when the user is not logged in (by checking the SecurityContext singleton) and instead only show the LoginView template, this is also helpful in cases where you want pages that only certain users have rights to see or use where you can check the security requirements before constructing the View, and replacing it with a security prompt.

抱歉回答太长,希望能帮到你:)

Sorry for the long answer, hope this helps :)

另外,您拼错了管理"

编辑评论中的问题

LoginManagerSingleton 如何直接与主窗口视图.不应该一切都经过MainWindowViewModel 以便后面没有代码主窗口视图

How would the LoginManagerSingleton talk directly to the MainWindowView. Shouldn't everything go through the MainWindowViewModel so that there is no code behind on the MainWindowView

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

Sorry, to clarify - I don't mean the LoginManager interacts directly with the MainWindowView (as this should be just-a-view), but rather that the LoginManager just sets a CurrentUser property in response to the call that the LoginCommand makes, which in turn raises the PropertyChanged event and the MainWindowView (which is listening for changes) reacts accordingly.

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

The LoginManager could then call PageManager.Open(new OverviewScreen()) (or PageManager.Open("overview.screen") when you have IOC implemented) for example to redirect the user to the default screen users see once logged in.

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

The LoginManager is essentially the last step of the actual login process and the View just reflects this as appropriate.

此外,在键入此内容时,我突然想到,所有这些都可以放在 PageManager 类中,而不是使用 LoginManager 单例.只需有一个 Login(string, string) 方法,它在成功登录时设置 CurrentUser.

Also, in typing this, it has occurred to me that rather than having a LoginManager singleton, all this could be housed in the PageManager class. Just have a Login(string, string) method, which sets the CurrentUser on successful log in.

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

I understand the idea of a PageManagerView, basically through a PageManagerViewModel

我不会将 PageManager 设计为 View-ViewModel 设计,只是一个实现 INotifyPropertyChanged 的普通家庭单例应该可以解决问题,这样 MainWindowView 可以对CurrentPage 属性.

I wouldn't design PageManager to be of View-ViewModel design, just an ordinary house-hold singleton that implements INotifyPropertyChanged should do the trick, this way the MainWindowView can react to the changing of the CurrentPage property.

ViewModelBase 是您创建的抽象类吗?

Is ViewModelBase an abstract class you created?

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

Yes. I use this class as the base class of all my ViewModel's.

这个类包含

  • 在所有页面上使用的属性,例如 Title、PageKey 和覆盖用户上下文.
  • 常见的虚方法,如 PageLoaded、PageDisplayed、PageSaved 和 PageClosed
  • 实现 INPC 并公开受保护的 OnPropertyChanged 方法以用于引发 PropertyChanged 事件
  • 并提供与页面交互的骨架命令,例如 ClosePageCommand、SavePageCommand 等.

当检测到登录时,CurrentControl 被设置为一个新的视图

When a logged in detected, CurrentControl is set to a new View

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

Personally, I would only hold the instance of the ViewModelBase that is currently being displayed. This is then referenced by the MainWindowView in a ContentControl like so: Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}".

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

I also then use a converter to transform the ViewModelBase instance in to a UserControl, but this is purely optional; You could just rely on ResourceDictionary entries, but this method also allows the developer to intercept the call and display a SecurityPage or ErrorPage if required.

然后当应用程序启动时它检测到没有人登录,并且因此创建一个 LoginView 并将其设置为 CurrentControl.而不是硬着头皮默认显示 LoginView

Then when the application starts it detects no one is logged in, and thus creates a LoginView and sets that to be the CurrentControl. Rather than harding it that the LoginView is displayed by default

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

You could design the application so that the first page that is displayed to the user is an instance of the OverviewScreen. Which, since the PageManager currently has a null CurrentUser property, the ViewModelToViewConverter would intercept this and the rather than display the OverviewScreenView UserControl, it would instead show the LoginView UserControl.

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

If and when the user successfully logs in, the LoginViewModel would instruct the PageManager to redirect to the original OverviewScreen instance, this time displaying correctly as the CurrentUser property is non-null.

人们如何像你提到的那样绕过这个限制,单身是不好的

How do people get around this limitation as you mention as do others, singletons are bad

我和你在一起,我喜欢单身.但是,这些的使用应仅限于仅在必要时使用.但在我看来,它们确实有完全有效的用途,但不确定是否有人想对此事发表意见?

I'm with you on this one, I like me a good singleton. However, the use of these should be limited to be used only where necessary. But they do have perfectly valid uses in my opinion, not sure if any one else wants to chime in on this matter though?

编辑 2:

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

Do you use a publicly available framework/set of classes for MVVM

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

No, I'm using a framework that I have created and refined over the last twelve months or so. The framework still follows most the MVVM guidelines, but includes some personal touches that reduces the amount of overall code required to be written.

例如,一些 MVVM 示例设置的视图与您的视图大致相同;而视图在其 ViewObject.DataContext 属性中创建了一个新的 ViewModel 实例.这可能对某些人有效,但不允许开发人员从 ViewModel 挂接某些 Windows 事件,例如 OnPageLoad().

For example, some MVVM examples out there set up their views much the same as you have; Whereas the View creates a new instance of the ViewModel inside of its ViewObject.DataContext property. This may work well for some, but doesn't allow the developer to hook certain Windows events from the ViewModel such as OnPageLoad().

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

OnPageLoad() in my case is called after all controls on the page have been created and have come in to view on screen, which may be instantly, within a few minutes after the constructor is called, or never at all. This is where I do most of my data loading to speed up the page loading process if that page has multiple child pages inside tabs that are not currently selected, for example.

但不仅如此,通过以这种方式创建 ViewModel,每个 View 中的代码量至少增加了三行.这听起来可能不多,但不仅这些代码行对于创建重复代码的所有视图来说本质上是相同的,而且如果您的应用程序需要很多行意见.那个,我真的很懒..我没有成为打代码的开发人员.

But not only that, by creating the ViewModel in this manner increases the amount of code in each View by a minimum of three lines. This may not sound like much, but not only are these lines of code essentially the same for all views creating duplicate code, but the extra line count can add up quite quickly if you have an application that requires many Views. That, and I'm really lazy.. I didn't become a developer to type code.

我将通过你对页面的想法在未来的修订中做什么manager 会像 tabcontrol 一样同时打开多个视图,页面管理器控制页面标签而不是单个用户控件.然后可以通过绑定到的单独视图选择选项卡页面管理器

What I will do in a future revision through your idea of a page manager would be to have several views open at once like a tabcontrol, where a page manager controls pagetabs instead of just a single userControl. Then tabs can be selected by a separate view binded to the page manager

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

In this case, the PageManager won't need to hold a direct reference to each of the open ViewModelBase classes, only those at the top-level. All other pages will be children of their parent to give you more control over the hierarchy and to allow you to trickle down Save and Close events.

如果您将这些放在 PageManager 中的 ObservableCollection 属性中,那么您只需要创建 MainWindow 的 TabControl,使其 ItemsSource 属性指向 PageManager 上的 Children 属性并具有WPF 引擎完成剩下的工作.

If you put these in an ObservableCollection<ViewModelBase> property in the PageManager, you will only then need to create the MainWindow's TabControl so that it's ItemsSource property points to the Children property on the PageManager and have the WPF engine do the rest.

你能在 ViewModelConverter 上再扩展一点吗

Can you expand a bit more on the ViewModelConverter

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

Sure, to give you an outline it would be easier to show some code.

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

分节阅读这段代码:

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

  • If value is null, return. Simple null reference check.
  • If the value is a ViewModelBase, and that page has already been loaded, just return that View. If you don't do this, you will be creating a new View each time the page is displayed and will cause some unexpected behaviour.
  • Get the page template UserControl (shown below)
  • Set the PageTemplate property so this instance can be hooked, and so we don't load a new instance on each pass.
  • Set the View DataContext to the ViewModel instance, these two lines completely replace those three lines I was talking about earlier from every view from this point on.
  • return the template. This will then be displayed in a ContentPresenter for the user to see.

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

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

This is the code in the converter that does most of the grunt work, reading through the sections you can see:

  • 主要 try..catch 块用于捕获任何类构造错误,包括,
    • 页面不存在,
    • 构造函数代码中的运行时错误,
    • 以及 XAML 中的致命错误.
    • 例如,SearchablePage 类将简单地显示系统中特定类型的所有对象的列表,并提供添加、编辑、刷新和过滤命令.
    • MaintenancePage 将从数据库中检索完整对象,为对象公开的字段动态生成和定位控件,根据对象拥有的任何集合创建子页面,并提供要使用的保存和删除命令.
    • 立>

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

    This all allows me to focus on only creating ViewModel classes as the application will simple display the default pages unless the View pages have been explicitly overridden by the developer for that ViewModel.

    这篇关于WPF MVVM - 简单登录到应用程序的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆