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

我将继续学习WPF,目前主要关注MVVM,并使用Karl Shifflett的“盒子中的MVVM”教程。但是有一个关于在视图/视图模型之间共享数据以及如何更新屏幕上的视图的问题。 ps。我还没有介绍IOC。

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

我认为该应用程序具有2个ViewModel。一个用于MainWindowView,一个用于LoginView,因为MainWindow不需要具有用于登录的命令,因此我将其分开。

由于我还没有介绍IOC,因此我创建了一个Singleton的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已处理该事件。您将如何创建一个新的View并将其放置在LoginView所在的位置,同样,一旦不需要它,您将如何处置LoginView。 MainWindowViewModel中是否会有诸如“ UserControl currentControl”之类的属性,该属性将设置为LoginView或OverviewScreenView。

Q3。应该在Visual Studio设计器中设置MainWindow的LoginView吗。或者应该将其留空,并以编程方式意识到没有人登录,因此一旦加载MainWindow,便会创建一个LoginView并将其显示在屏幕上。

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

主窗口的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();
        }
    }
}

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

参考方案

长期以来的问题,蝙蝠侠!

Q1:
该过程将正常工作,但是我不知道使用LoginModelMainWindowViewModel进行通信。

您可以尝试类似LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView

我知道单例被某些人视为反模式,但是我发现这种情况最简单。这样,单例类可以实现INotifyPropertyChanged接口,并在检测到login \ out事件时引发事件。

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

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

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

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

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

抱歉,答案很长,希望对您有所帮助:)

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

编辑评论中的问题

LoginManagerSingleton将如何直接与
MainWindowView。一切都不应该经历
MainWindowViewModel,因此在其上没有任何代码
主窗口视图

抱歉,需要澄清一下-我并不是说LoginManager与MainWindowView直接交互(因为这应该只是一个视图),而是LoginManager只是设置一个CurrentUser属性来响应LoginCommand的调用,这又引发了PropertyChanged事件,并且MainWindowView(正在侦听更改)做出了相应的反应。

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

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

另外,在键入此内容时,我想到,所有这些都可以存放在PageManager类中,而不是使用LoginManager单例。只需使用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的实例。然后由MainWindowView在ContentControl中进行引用,如下所示: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使用公开可用的框架/类集

不,我使用的是过去十二个月左右创建并完善的框架。该框架仍然遵循大多数MVVM准则,但包含一些个人方面的内容,从而减少了需要编写的总体代码量。

例如,一些MVVM示例设置了与您所拥有的视图几乎相同的视图。而View在其ViewObject.DataContext属性内创建ViewModel的新实例。对于某些人来说,这可能很好用,但不允许开发人员从ViewModel挂钩某些Windows事件,例如OnPageLoad()。

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

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

通过您对页面的想法,我将来会做些什么
经理将像tabcontrol一样一次打开多个视图,
页面管理员可以控制页面标签,而不仅仅是一个
userControl。然后可以通过绑定到的单独视图来选择选项卡
页面管理员

在这种情况下,PageManager不需要保留对每个打开的ViewModelBase类的直接引用,而仅需要保留对顶级类的直接引用。所有其他页面都是其父级的子级,以使您能够更好地控制层次结构并允许您滴入Save和Close事件。

如果将它们放在PageManager的ObservableCollection<ViewModelBase>属性中,则只需要创建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类型创建默认页面。我还有一些其他的ViewModel基类,这些基类继承自ViewModelBase,它们提供了页面类型之间的职责分离。

例如,SearchablePage类将仅显示某种类型的系统中所有对象的列表,并提供“添加”,“编辑”,“刷新”和“过滤器”命令。
MaintenancePage将从数据库中检索完整的对象,为该对象公开的字段动态生成和定位控件,基于该对象具有的任何集合创建子页面,并提供要使用的Save和Delete命令。

如果我们仍然没有模板可以使用,则抛出一个错误,以便开发人员知道出了点问题。
在catch块中,发生的任何运行时错误都会在友好的ErrorPage中显示给用户。

所有这些使我可以只专注于创建ViewModel类,因为应用程序将简单地显示默认页面,除非开发人员已针对该ViewModel明确覆盖了View页面。

如何为WPF饼图中的饼图切片设置默认颜色 - c#

我正在使用Zag studio中的文章将WPF饼图与标签一起使用。此图表每1分钟刷新一次新值。效果很好,但是为什么每次刷新饼图的颜色都会改变?有没有办法设置默认颜色。我显示的饼图只有两个部分。我尝试过的 <customControls:LabeledPieChart> <customControls:LabeledPieChart.Pale…

如何在WPF中处理TabItem单击事件? - c#

在我的应用程序中,我使用了WPF TabControl,我想处理TabItem的click事件。我该如何实现? 参考方案 您可以通过在tabcontrol中的每个tabitem的header属性中添加标签来实现。然后,您可以为标签设置一个事件。a<TabControl Height="100" HorizontalAlignment…

ICommand以MVVM样式进行按钮按下和释放 - c#

我很高兴使用ICommand实现来处理按钮上的单个操作:<Button Command="{Binding Path=ActionDown}">Press Me</Button> 通过ICommand实现RelayCommand但是我找不到一种简单的方法来同时为新闻发布和发布提供动作(在SO上以及在其他网站上)。 …

在datagrid(wpf)中选择行索引 - c#

如何在datagrid中选择行索引? 事件SelectionChanged以下代码不起作用: private DataGridRow dgr = new DataGridRow(); private void dataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { thi…

Div单击与单选按钮相同吗? - php

有没有一种方法可以使div上的click事件与表单环境中的单选按钮相同?我只希望下面的div提交值,单选按钮很丑代码输出如下:<input id="radio-2011-06-08" value="2011-06-08" type="radio" name="radio_date&#…