WPF MVVM - простой вход в приложение - PullRequest
22 голосов
/ 19 августа 2011

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

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

Main Window

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

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

Конструктор MainWindowViewModel регистрируется в событии UserLoggedIn. Теперь в LoginView, когда пользователь нажимает Login на LoginView, он вызывает команду на 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. Должно ли 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
    }
}

1 Ответ

28 голосов
/ 19 августа 2011

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

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

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

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

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

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

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

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

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

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

Edit: Также вы ошиблись "Управление"

<ч />

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

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

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

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

LoginManager, по сути, является последним этапом реального процесса входа в систему, и представление просто отражает это соответствующим образом.

Кроме того, при наборе этого слова мне пришло в голову, что вместо использования синглтона 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, который отображается в данный момент.На это затем ссылается MainWindowView в ContentControl следующим образом: Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}".

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

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

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

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

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

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

Редактировать 2:

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

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

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

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

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

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

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

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

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

  • Если значение равно нулю, вернуть.Простая проверка нулевой ссылки.
  • Если значением является ViewModelBase, и эта страница уже загружена, просто верните это представление.Если вы этого не сделаете, вы будете создавать новый просмотр каждый раз, когда отображается страница, и вызывать непредвиденное поведение.
  • Получить шаблон страницы 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, и возвращает код типа, который, по его мнению, должен быть (это может быть нулем).
  • Если это не NULL, создайте новый экземпляр типа.
  • Если нам не удается найти представление для использования, попробуйте создать страницу по умолчанию для этого типа ViewModel.У меня есть несколько дополнительных базовых классов ViewModel, которые наследуются от ViewModelBase, которые обеспечивают разделение обязанностей между типами страниц, которыми они являются.
    • Например, класс SearchablePage просто отобразит список всех объектов в системе определенного типа и предоставит команды Add, Edit, Refresh и Filter.
    • MaintenancePage будет получатьполный объект из базы данных, динамически генерировать и позиционировать элементы управления для полей, которые предоставляет объект, создает дочерние страницы на основе любой коллекции, имеющейся у объекта, и предоставляет команды «Сохранить» и «Удалить» для использования.
  • Если у нас все еще нет шаблона для использования, выведите ошибку, чтобы разработчик знал, что что-то пошло не так.
  • В блоке catch любая возникающая ошибка во время выполнения отображается пользователю в видеFriendly ErrorPage.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...