Подтвердить что ты не робот

WPF MVVM - простой вход в приложение

Я продолжаю изучать WPF и фокусируюсь на MVVM на данный момент и используя учебник "MVVM In a Box" Карла Шиффлетта. Но у вас есть вопрос об обмене данными между видами/режимами просмотра и тем, как он обновляет представление на экране. постскриптум Я еще не рассматривал МОК.

Ниже приведен снимок экрана моего MainWindow в тестовом приложении. Его разделение на 3 раздела (представления), заголовок, панель слайдов с кнопками, а остальная часть - как основной вид приложения. Цель приложения проста, войдите в приложение. При успешном входе в систему имя входа в систему должно исчезнуть, заменив его новым видом (т.е. OverviewScreenView), и соответствующие кнопки на слайде приложения должны стать видимыми.

Main Window

Я вижу приложение как имеющее 2 ViewModels. Один для MainWindowView и один для LoginView, учитывая, что MainWindow не нуждается в командах для входа, поэтому я сохранил его отдельно.

Поскольку я еще не рассматривал IOC, я создал класс LoginModel, который является одноэлементным. Он содержит только одно свойство, которое является "public bool LoggedIn", и событие с именем UserLoggedIn.

Конструктор MainWindowViewModel регистрируется в событии UserLoggedIn. Теперь в LoginView, когда пользователь нажимает "Вход в LoginView", он вызывает команду LoginLogModel, которая, в свою очередь, если имя пользователя и пароль правильно введены, вызовет LoginModel и установит LoggedIn в значение true. Это приводит к срабатыванию события UserLoggedIn, которое обрабатывается в MainWindowViewModel, чтобы заставить представление скрыть LoginView и заменить его другим видом, то есть обзорным экраном.

Вопросы

Q1. Очевидный вопрос заключается в правильном использовании MVVM. т.е. поток управления выглядит следующим образом. LoginView → LoginViewViewModel → LoginModel → MainWindowViewModel → MainWindowView.

Q2. Предполагая, что пользователь вошел в систему, и MainWindowViewModel обработал событие. Как бы вы начали создавать новый View и помещать его там, где LoginView был, в равной степени, как вы собираетесь избавляться от LoginView, когда он не нужен. Будет ли свойство в MainWindowViewModel, например, "UserControl currentControl", которое устанавливается в LoginView или OverviewScreenView.

Q3. Если в MainWindow установлен элемент LoginView в дизайнере визуальной студии. Или, если он оставлен пустым и программно он понимает, что никто не зарегистрирован, поэтому после загрузки MainWindow он создает LoginView и показывает его на экране.

Некоторые примеры кода ниже, если это помогает с ответом на вопросы

XAML для 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
    }
}
4b9b3361

Ответ 1

Священный длинный вопрос, Бэтмен!

Q1: Этот процесс будет работать, но я не знаю, как использовать LoginModel для связи с MainWindowViewModel.

Вы можете попробовать что-то вроде LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView

Я знаю, что одноэлемент считается некоторыми анти-шаблонами, но я считаю, что это проще всего для таких ситуаций. Таким образом, singleton-класс может реализовывать интерфейс INotifyPropertyChanged и вызывать события всякий раз, когда происходит событие login\out.

Внедрите LoginCommand либо в LoginViewModel, либо в Singleton (Лично я бы, вероятно, реализовал это на ViewModel, чтобы добавить степень разделения между классами ViewModel и "back-end" ). Эта команда входа вызовет метод на синглете, чтобы выполнить вход.

Q2: В этих случаях у меня обычно есть (еще один) singleton-класс, чтобы действовать как PageManager или ViewModelManager. Этот класс отвечает за создание, удаление и хранение ссылок на страницы верхнего уровня или CurrentPage (только в одной странице).

У моего класса ViewModelBase также есть свойство удерживать текущий экземпляр UserControl, который отображает мой класс, поэтому я могу подключить события Loaded and Unloaded. Это дает мне возможность иметь виртуальные методы OnLoaded(), OnDisplayed() and OnClosed(), которые можно определить в ViewModel, чтобы страница могла выполнять операции погрузки и разгрузки.

Поскольку MainWindowView отображает экземпляр ViewModelManager.CurrentPage, как только этот экземпляр изменится, произойдет событие Unloaded, вызывается моя утилита Dispose, и в конечном итоге GC приходит и убирает остальные.

Q3: Я не уверен, понимаю ли я это, но, надеюсь, вы просто имеете в виду "Показывать страницу входа в систему, когда пользователь не вошел в систему", если это так, вы можете указать вашему ViewModelToViewConverter игнорировать любые инструкции, когда пользователь не регистрируется (путем проверки SingleContext SecurityContext) и вместо этого отображает только шаблон LoginView, это также полезно в тех случаях, когда вам нужны страницы, на которых только определенные пользователи имеют права видеть или использовать, где вы можете проверить требования безопасности, прежде чем создавать представление, и заменив его подсказкой безопасности.

Извините за длинный ответ, надеюсь, что это поможет:)

Изменить: Кроме того, у вас есть ошибочное "Управление"


Изменить для комментариев в комментариях

Как бы LoginManagerSingleton напрямую разговаривал с MainWindowView. Не все должно пройти через MainWindowViewModel, чтобы на MainWindowView

Извините, чтобы уточнить - я не имею в виду, что LoginManager взаимодействует напрямую с MainWindowView (поскольку это должен быть просто-вид), а скорее, что LoginManager просто устанавливает свойство CurrentUser в ответ на вызов, который loginCommand делает, что, в свою очередь, вызывает событие PropertyChanged, и MainWindowView (который прослушивает изменения) реагирует соответственно.

Затем LoginManager может вызывать PageManager.Open(new OverviewScreen()) (или PageManager.Open("overview.screen") при реализации IOC), например, для перенаправления пользователя на экран, который пользователи по умолчанию просматривают после входа в систему.

LoginManager - это, по сути, последний шаг фактического процесса входа в систему, и представление просто отражает это, если это необходимо.

Кроме того, при вводе этого мне пришло в голову, что вместо того, чтобы иметь singleton 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 установлен на новый View

Лично я бы использовал только экземпляр ViewModelBase, который в настоящее время отображается. Затем этот файл ссылается на MainWindowView в ContentControl следующим образом: Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}".

Затем я также использую конвертер для преобразования экземпляра ViewModelBase в UserControl, но это чисто необязательно; Вы можете просто полагаться на записи ResourceDictionary, но этот метод также позволяет разработчику перехватывать вызов и отображать при необходимости SecurityPage или ErrorPage.

Затем, когда приложение запускается, он обнаруживает, что никто не вошел в систему, и таким образом, создает LoginView и устанавливает, что это CurrentControl. Вместо того, чтобы упростить его, что LoginView отображается по умолчанию

Вы можете создать приложение таким образом, чтобы первая страница, отображаемая пользователю, являлась экземпляром OverviewScreen. Что, поскольку в настоящее время свойство PageManager имеет нулевое свойство CurrentUser, ViewModelToViewConverter будет перехватывать это, а вместо отображать UserSontrol OverviewScreenView вместо этого будет отображаться UserView UserControl.

Если и когда пользователь успешно войдет в систему, LoginViewModel будет указывать, что PageManager перенаправляется на исходный экземпляр OverviewScreen, на этот раз правильно отображаемый, поскольку свойство CurrentUser не равно null.

Как люди обходят это ограничение, как вы упоминаете, как и другие, одиночные игры плохие

Я с тобой на этом, мне нравится хороший синглтон. Однако их использование должно быть ограничено только в случае необходимости. Но у них действительно есть правильные варианты использования, на мой взгляд, не уверены, хочет ли кто-нибудь другой в этом вопросе?


Изменить 2:

Используете ли вы общедоступную среду/набор классов для MVVM

Нет, я использую фреймворк, который я создал и уточнил за последние двенадцать месяцев или около того. Структура по-прежнему соответствует большинству руководств MVVM, но включает в себя некоторые личные прикосновения, которые уменьшают объем общего кода, который требуется записать.

Например, некоторые примеры MVVM устанавливают свои представления так же, как и у вас; В то время как представление создает новый экземпляр ViewModel внутри его свойства ViewObject.DataContext. Это может хорошо работать для некоторых, но не позволяет разработчику подключать определенные события Windows из ViewModel, такие как OnPageLoad().

OnPageLoad() в моем случае вызывается после того, как все элементы управления на странице были созданы и появились на экране, что может быть мгновенно, в течение нескольких минут после вызова конструктора или вообще никогда. Вот где я делаю большую часть загрузки данных, чтобы ускорить процесс загрузки страницы, если на этой странице есть несколько дочерних страниц внутри вкладок, которые в настоящее время не выбраны.

Но не только это, создавая ViewModel таким образом, увеличивает количество кода в каждом представлении минимум на три строки. Это может показаться не очень похожим, но не только эти строки кода по существу одинаковы для представлений all, создающих повторяющийся код, но дополнительный счетчик строк может складываться довольно быстро, если у вас есть приложение, которое требует много Просмотры. Это, и я действительно ленив.. Я не стал разработчиком, чтобы набирать код.

Что я буду делать в будущей редакции через вашу идею страницы менеджер должен был сразу открыть несколько просмотров, как tabcontrol, где менеджер страниц управляет pagetabs, а не только одним UserControl. Затем вкладки могут быть выбраны отдельным представлением, привязанным к менеджер страниц

В этом случае для PageManager не потребуется прямая ссылка на каждый из открытых классов ViewModelBase, только на верхнем уровне. Все остальные страницы будут дочерними элементами их родителей, чтобы дать вам больше контроля над иерархией и позволить вам просачивать события "Сохранить и закрыть".

Если вы поместите их в свойство ObservableCollection<ViewModelBase> в PageManager, тогда вам нужно будет только создать MainWindow TabControl, чтобы свойство ItemsSource указывало на свойство Children на страницеManager и все остальное выполняло механизм 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;
    }

Прочитав этот код в разделах, он читает:

  • Если значение равно null, верните. Простая нулевая проверка ссылок.
  • Если значение представляет собой ViewModelBase, и эта страница уже загружена, просто верните этот вид. Если вы этого не сделаете, вы будете создавать новый просмотр каждый раз, когда страница будет отображаться, и это приведет к неожиданному поведению.
  • Получить шаблон страницы UserControl (показано ниже)
  • Задайте свойство PageTemplate, чтобы этот экземпляр можно было подключить, и поэтому мы не загружаем новый экземпляр на каждом проходе.
  • Установите ViewConttext ViewModel в экземпляр 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;
    }
    

Это код в конвертере, который выполняет большую часть работы grunt, просматривая разделы, которые вы можете видеть:

  • Основной блок try..catch, используемый для обнаружения любых ошибок построения класса, включая,
    • Страница не существует,
    • Ошибка времени выполнения в коде конструктора,
    • И фатальные ошибки в XAML.
  • convertViewModelTypeToViewType() просто пытается найти представление, соответствующее ViewModel, и возвращает код типа, который, по его мнению, должен быть (это может быть null).
  • Если это не null, создайте новый экземпляр типа.
  • Если нам не удается найти View для использования, попробуйте создать страницу по умолчанию для этого типа ViewModel. У меня есть несколько дополнительных базовых классов ViewModel, которые наследуют от ViewModelBase, которые обеспечивают разделение обязанностей между типами страниц, которыми они являются.
    • Например, класс SearchablePage будет просто отображать список всех объектов в системе определенного типа и предоставлять команды "Добавить, редактировать, обновлять и фильтровать".
    • MaintenancePage будет извлекать полный объект из базы данных, динамически генерировать и размещать элементы управления для полей, которые предоставляет объект, создает дочерние страницы на основе любой коллекции, которую имеет объект, и предоставляет команды сохранения и удаления для использования.
  • Если у нас по-прежнему нет шаблона для использования, запустите ошибку, чтобы разработчик знал, что что-то пошло не так.
  • В блоке catch любая ошибка времени выполнения отображается пользователю в дружественной ErrorPage.

Это позволяет мне сосредоточиться только на создании классов ViewModel, поскольку приложение будет просто отображать страницы по умолчанию, если только страницы View явно не переопределены разработчиком для этой модели ViewModel.