web-dev-qa-db-fra.com

WPF MVVM - Connexion simple à une application

Je continue à apprendre WPF, je me concentre sur MVVM en ce moment et j'utilise le tutoriel "MVVM In a Box" de Karl Shifflett. Mais vous avez une question sur le partage de données entre les vues/modèles de vue et comment il met à jour la vue à l'écran. p.s. Je n'ai pas encore couvert le CIO.

Voici une capture d'écran de ma MainWindow dans une application de test. Il est divisé en 3 sections (vues), un en-tête, un panneau coulissant avec des boutons et le reste comme vue principale de l'application. Le but de l'application est simple, connectez-vous à l'application. En cas de connexion réussie, la vue de connexion devrait disparaître en étant remplacée par une nouvelle vue (c'est-à-dire OverviewScreenView), et les boutons appropriés sur la diapositive de l'application devraient devenir visibles.

Main Window

Je vois l'application comme ayant 2 ViewModels. Un pour le MainWindowView et un pour le LoginView, étant donné que le MainWindow n'a pas besoin d'avoir de commandes pour la connexion, je l'ai donc gardé séparé.

Comme je n'ai pas encore couvert les IOC, j'ai créé une classe LoginModel qui est un singleton. Il ne contient qu'une seule propriété qui est "public bool LoggedIn" et un événement appelé UserLoggedIn.

Le constructeur MainWindowViewModel s'inscrit à l'événement UserLoggedIn. Maintenant, dans le LoginView, lorsqu'un utilisateur clique sur Login sur le LoginView, il lève une commande sur le LoginViewModel, qui à son tour si un nom d'utilisateur et un mot de passe sont correctement entrés appellera le LoginModel et définira LoggedIn sur true. Cela provoque le déclenchement de l'événement UserLoggedIn, qui est géré dans MainWindowViewModel pour que la vue masque la LoginView et la remplace par une autre vue, c'est-à-dire un écran de présentation.

Questions

Q1. Une question évidente, est de se connecter comme ceci une utilisation correcte de MVVM. c'est-à-dire que le flux de contrôle est le suivant. LoginView -> LoginViewViewModel -> LoginModel -> MainWindowViewModel -> MainWindowView.

Q2. En supposant que l'utilisateur s'est connecté et que MainWindowViewModel a géré l'événement. Comment feriez-vous pour créer une nouvelle vue et la placer là où se trouvait le LoginView, de même comment procéder pour éliminer le LoginView une fois qu'il n'est pas nécessaire. Y aurait-il une propriété dans MainWindowViewModel comme "UserControl currentControl", qui est définie sur LoginView ou un OverviewScreenView.

Q3. Si la fenêtre principale a un LoginView défini dans le concepteur de studio visuel. Ou doit-il être laissé vide, et par programme, il se rend compte que personne n'est connecté, donc une fois que la fenêtre principale est chargée, il crée une LoginView et l'affiche à l'écran.

Quelques exemples de code ci-dessous si cela aide à répondre aux questions

XAML pour la fenêtre principale

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

Sainte longue question, Batman!

Q1: Le processus fonctionnerait, je ne sais pas comment utiliser le LoginModel pour parler au MainWindowViewModel cependant.

Vous pouvez essayer quelque chose comme LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView

Je sais que les singleton sont considérés comme anti-patterns par certains, mais je trouve que c'est plus facile pour des situations comme celles-ci. De cette façon, la classe singleton peut implémenter l'interface INotifyPropertyChanged et déclencher des événements chaque fois qu'un événement login\out est détecté.

Implémentez le LoginCommand sur le LoginViewModel ou le Singleton (Personnellement, je l'implémenterais probablement sur le ViewModel pour ajouter un degré de séparation entre le ViewModel et le "back-" fin "classes utilitaires). Cette commande de connexion appelle une méthode sur le singleton pour effectuer la connexion.

Q2: Dans ces cas, j'ai généralement (encore une autre) classe singleton pour agir comme PageManager ou ViewModelManager. Cette classe est responsable de la création, de la suppression et de la conservation des références aux pages de niveau supérieur ou à CurrentPage (dans une situation d'une seule page).

Ma classe ViewModelBase possède également une propriété pour contenir l'instance actuelle du UserControl qui affiche ma classe, c'est pour que je puisse accrocher les événements Loaded et Unloaded. Cela me donne la possibilité d'avoir des méthodes virtuelles OnLoaded(), OnDisplayed() and OnClosed() qui peuvent être définies dans ViewModel afin que la page puisse effectuer des actions de chargement et de déchargement.

Comme MainWindowView affiche l'instance ViewModelManager.CurrentPage, Une fois que cette instance change, l'événement Unloaded se déclenche, la méthode Dispose de ma page est appelée et finalement GC entre et range le reste.

Q3: Je ne sais pas si je comprends celui-ci, mais j'espère que vous voulez simplement dire "Afficher la page de connexion lorsque l'utilisateur n'est pas connecté", si tel est le cas, vous pouvez demander à votre ViewModelToViewConverter pour ignorer toutes les instructions lorsque l'utilisateur n'est pas connecté (en vérifiant le singleton SecurityContext) et n'afficher à la place que le modèle LoginView, cela est également utile dans les cas où vous souhaitez des pages auxquelles seuls certains utilisateurs ont des droits voir ou utiliser où vous pouvez vérifier les exigences de sécurité avant de construire la vue et de la remplacer par une invite de sécurité.

Désolé pour la longue réponse, j'espère que cela vous aidera :)

Edit: De plus, vous avez mal orthographié "Gestion"


Modifier pour les questions dans les commentaires

Comment le LoginManagerSingleton parlerait-il directement à MainWindowView? Tout ne doit-il pas passer par le MainWindowViewModel pour qu'il n'y ait pas de code derrière sur le MainWindowView

Désolé, pour clarifier - je ne veux pas dire que le LoginManager interagit directement avec MainWindowView (car cela devrait être juste une vue), mais plutôt que le LoginManager définit simplement une propriété CurrentUser en réponse à l'appel qui fait le LoginCommand, qui à son tour déclenche l'événement PropertyChanged et le MainWindowView (qui écoute les modifications) réagit en conséquence.

Le LoginManager pourrait alors appeler PageManager.Open(new OverviewScreen()) (ou PageManager.Open("overview.screen") lorsque vous avez IOC implémenté) par exemple pour rediriger l'utilisateur vers l'écran par défaut que les utilisateurs voient une fois connecté dans.

Le LoginManager est essentiellement la dernière étape du processus de connexion proprement dit et la vue le reflète comme il convient.

De plus, en tapant ceci, il m'est venu à l'esprit que plutôt que d'avoir un singleton LoginManager, tout cela pourrait être hébergé dans la classe PageManager. Il suffit d'avoir une méthode Login(string, string), qui définit le CurrentUser en cas de connexion réussie.

Je comprends l'idée d'un PageManagerView, essentiellement via un PageManagerViewModel

Je ne concevrais pas PageManager comme étant de conception View-ViewModel, juste un singleton domestique ordinaire qui implémente INotifyPropertyChanged devrait faire l'affaire, de cette façon le MainWindowView peut réagir au changement de la propriété CurrentPage.

ViewModelBase est-il une classe abstraite que vous avez créée?

Oui. J'utilise cette classe comme classe de base de tous mes ViewModel.

Cette classe contient

  • Propriétés utilisées sur toutes les pages telles que Title, PageKey et OverriddenUserContext.
  • Méthodes virtuelles courantes telles que PageLoaded, PageDisplayed, PageSaved et PageClosed
  • Implémente INPC et expose une méthode OnPropertyChanged protégée à utiliser pour déclencher l'événement PropertyChanged
  • Et fournit des commandes squelettes pour interagir avec la page telles que ClosePageCommand, SavePageCommand etc.

Lorsqu'une connexion est détectée, CurrentControl est défini sur une nouvelle vue

Personnellement, je ne conserverais que l'instance de ViewModelBase qui est actuellement affichée. Ceci est ensuite référencé par le MainWindowView dans un ContentControl comme ceci: Content="{Binding Source={x:Static vm:PageManager.Current}, Path=CurrentPage}".

J'utilise également un convertisseur pour transformer l'instance de ViewModelBase en un UserControl, mais c'est purement facultatif; Vous pouvez simplement vous fier aux entrées ResourceDictionary, mais cette méthode permet également au développeur d'intercepter l'appel et d'afficher une SecurityPage ou une ErrorPage si nécessaire.

Puis, lorsque l'application démarre, elle détecte que personne n'est connecté, et crée ainsi un LoginView et définit celui-ci comme étant le CurrentControl. Plutôt que de le durcir, le LoginView est affiché par défaut

Vous pouvez concevoir l'application de sorte que la première page qui s'affiche pour l'utilisateur soit une instance de OverviewScreen. Ce qui, puisque le PageManager a actuellement une propriété CurrentUser nulle, le ViewModelToViewConverter intercepterait cela et au lieu d'afficher le OverviewScreenView UserControl, il afficherait plutôt le LoginView UserControl.

Si et lorsque l'utilisateur se connecte avec succès, le LoginViewModel demanderait au PageManager de rediriger vers l'instance OverviewScreen d'origine, cette fois s'affichant correctement car la propriété CurrentUser n'est pas nulle.

Comment les gens contournent-ils cette limitation comme vous le mentionnez comme les autres, les singletons sont mauvais

Je suis avec toi sur celui-ci, je m'aime bien un singleton. Cependant, leur utilisation devrait être limitée pour n'être utilisée qu'en cas de besoin. Mais ils ont des utilisations parfaitement valides à mon avis, vous ne savez pas si quelqu'un d'autre veut intervenir sur cette question?


Modifier 2:

Utilisez-vous un cadre/ensemble de classes accessible au public pour MVVM

Non, j'utilise un framework que j'ai créé et affiné au cours des douze derniers mois environ. Le cadre suit toujours la plupart des directives MVVM, mais inclut quelques touches personnelles qui réduisent la quantité de code global à écrire.

Par exemple, certains exemples MVVM là-bas configurent leurs vues de la même manière que vous; Alors que la vue crée une nouvelle instance du ViewModel à l'intérieur de sa propriété ViewObject.DataContext. Cela peut bien fonctionner pour certains, mais ne permet pas au développeur de connecter certains événements Windows à partir du ViewModel tels que OnPageLoad ().

OnPageLoad () dans mon cas est appelé après que tous les contrôles de la page ont été créés et sont venus s'afficher à l'écran, ce qui peut être instantanément, quelques minutes après l'appel du constructeur, ou jamais du tout. C'est là que je fais la plupart de mes données de chargement pour accélérer le processus de chargement de page si cette page a plusieurs pages enfants dans des onglets qui ne sont pas actuellement sélectionnés, par exemple.

Mais pas seulement cela, en créant le ViewModel de cette manière augmente la quantité de code dans chaque vue d'un minimum de trois lignes. Cela peut ne pas sembler beaucoup, mais non seulement ces lignes de code sont essentiellement les mêmes pour les vues toutes créant du code en double, mais le nombre de lignes supplémentaires peut s'additionner assez rapidement si vous avez une application qui nécessite beaucoup Vues. Ça, et je suis vraiment paresseux .. Je ne suis pas devenu développeur pour taper du code.

Ce que je ferai dans une future révision à travers votre idée d'un gestionnaire de pages serait d'avoir plusieurs vues ouvertes à la fois comme un tabcontrol, où un gestionnaire de pages contrôle les onglets au lieu d'un seul userControl. Ensuite, les onglets peuvent être sélectionnés par une vue distincte liée au gestionnaire de pages

Dans ce cas, le PageManager n'aura pas besoin de contenir une référence directe à chacune des classes ViewModelBase ouvertes, uniquement à celles du niveau supérieur. Toutes les autres pages seront des enfants de leur parent pour vous donner plus de contrôle sur la hiérarchie et pour vous permettre de diffuser les événements Enregistrer et Fermer.

Si vous les placez dans une propriété ObservableCollection<ViewModelBase> Dans le PageManager, vous n'aurez alors qu'à créer TabControl de MainWindow pour que sa propriété ItemsSource pointe vers la propriété Children du PageManager et que le moteur WPF fasse le reste.

Pouvez-vous développer un peu plus le ViewModelConverter

Bien sûr, pour vous donner un aperçu, il serait plus facile d'afficher du 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;
    }

En lisant ce code dans les sections, il lit:

  • Si la valeur est nulle, retournez. Vérification de référence nulle simple.
  • Si la valeur est un ViewModelBase et que cette page a déjà été chargée, renvoyez simplement cette vue. Si vous ne le faites pas, vous créerez une nouvelle vue chaque fois que la page sera affichée et provoquera un comportement inattendu.
  • Obtenez le modèle de page UserControl (illustré ci-dessous)
  • Définissez la propriété PageTemplate pour que cette instance puisse être accrochée et pour que nous ne chargions pas de nouvelle instance à chaque passage.
  • Définissez View DataContext sur l'instance ViewModel, ces deux lignes remplacent complètement les trois lignes dont je parlais plus tôt à partir de chaque vue à partir de ce point.
  • retourner le modèle. Cela sera ensuite affiché dans un ContentPresenter pour que l'utilisateur puisse le voir.

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

C'est le code dans le convertisseur qui fait la plupart du travail de grognement, en lisant les sections que vous pouvez voir:

  • Bloc try..catch principal utilisé pour intercepter toutes les erreurs de construction de classe, y compris,
    • La page n'existe pas,
    • Erreur d'exécution dans le code constructeur,
    • Et des erreurs fatales dans XAML.
  • convertViewModelTypeToViewType () essaie simplement de trouver la vue qui correspond à ViewModel et retourne le code de type qu'il pense qu'il devrait être (cela peut être nul).
  • Si ce n'est pas null, créez une nouvelle instance du type.
  • Si nous ne parvenons pas à trouver une vue à utiliser, essayez de créer la page par défaut pour ce type de ViewModel. J'ai quelques classes de base ViewModel supplémentaires qui héritent de ViewModelBase et qui offrent une séparation des tâches entre les types de pages qu'elles sont.
    • Par exemple, une classe SearchablePage affichera simplement une liste de tous les objets du système d'un certain type et fournira les commandes Ajouter, Modifier, Actualiser et Filtrer.
    • Un MaintenancePage récupérera l'objet complet de la base de données, générera et positionnera dynamiquement les contrôles pour les champs que l'objet expose, crée des pages enfants en fonction de toute collection de l'objet et fournit les commandes Enregistrer et Supprimer à utiliser.
  • Si nous n'avons toujours pas de modèle à utiliser, lancez une erreur pour que le développeur sache que quelque chose s'est mal passé.
  • Dans le bloc catch, toute erreur d'exécution qui se produit est affichée à l'utilisateur dans une page d'erreur conviviale.

Tout cela me permet de me concentrer uniquement sur la création de classes ViewModel car l'application affichera simplement les pages par défaut, sauf si les pages View ont été explicitement remplacées par le développeur pour ce ViewModel.

28
fatty