WPF MVVM - Simple login to an application

49,288

Holy long question, Batman!

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

You could try something like LoginView -> LoginViewModel -> [SecurityContextSingleton || LoginManagerSingleton] -> MainWindowView

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.

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.

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).

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.

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.

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 :)

Edit: Also, you have misspelled "Management"


Edit for questions in comments

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

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.

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.

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

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.

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

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.

Is ViewModelBase an abstract class you created?

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

This class contains

  • Properties that are used on all pages such as Title, PageKey and OverriddenUserContext.
  • Common virtual methods such as PageLoaded, PageDisplayed, PageSaved and PageClosed
  • Implements INPC and exposes a protected OnPropertyChanged method to use to raise the PropertyChanged event
  • And provides skeleton commands to interact with the page such as ClosePageCommand, SavePageCommand etc.

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

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

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.

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

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.

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?


Edit 2:

Do you use a publicly available framework/set of classes for 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.

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() 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.

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.

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

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.

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.

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

Reading through this code in sections it reads:

  • 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:

  • Main try..catch block used to catch any class construction errors including,
    • Page does not exist,
    • Run-time error in constructor code,
    • And fatal errors in XAML.
  • convertViewModelTypeToViewType() just tries to find the View that corresponds to the ViewModel and returns the type code that it thinks it should be (this may be null).
  • If this is not null, create a new instance of the type.
  • If we fail to find a View to use, try to create the default page for that ViewModel type. I have a few additional ViewModel base classes that inherit from ViewModelBase that provide a separation of duties between the types of page's they are.
    • For example, a SearchablePage class will simply display a list of all objects in the system of a certain type and provide the Add, Edit, Refresh and Filter commands.
    • A MaintenancePage will retrieve the full object from the database, dynamically generate and position the controls for the fields that the object exposes, creates children pages based on any collection the object has and provides the Save and Delete commands to use.
  • If we still don't have a template to use, throw an error so the developer knows something went wrong.
  • In the catch block, any run-time error that occurs are displayed to the user in a friendly ErrorPage.

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.

Share:
49,288

Related videos on Youtube

JonWillis
Author by

JonWillis

Updated on August 20, 2020

Comments

  • JonWillis
    JonWillis over 3 years

    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.

    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.

    Main Window

    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.

    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.

    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.

    Questions

    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.

    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.

    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 for the 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
        }
    }
    
  • JonWillis
    JonWillis over 12 years
    Q1. 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
  • JonWillis
    JonWillis over 12 years
    Q2. I understand the idea of a PageManagerView, basically through a PageManagerViewModel this would hold a reference to the currently displayed user control, through a property CurrentControl. When a logged in detected, CurrentControl is set to a new View, and within the view the XAML creates its own ViewModel to control it. p.s. Is ViewModelBase an abstract class you created?
  • JonWillis
    JonWillis over 12 years
    Q3. In reference to the PageManagerView above. I meant should the application be created in Visual studio with a PageManagerView. 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
  • JonWillis
    JonWillis over 12 years
    Q4. Yes management was intentionally mis-spelled to try and catch people out. On a more serious note, I use Singletons a fair bit to gain access to important classes anywhere that should only exist once. I.e. PageManager. How do people get around this limitation as you mention as do others, singletons are bad.
  • fatty
    fatty over 12 years
    @JonWillis I have edited my question as per your comments. We can discuss this in chat some more at some stage if you want as well.
  • JonWillis
    JonWillis over 12 years
    Excellent response.I think I see it more clearly now. PageManager is just a singleton class,it is not a View nor a ViewModel. PageManager will have a CurrentControl and CurrentUser properties. The LoginView --> ViewModel will update the PageManager property CurrentUser (ideally through a service class i guess). This is where I need clarification. Is CurrentUser binded to the MainWindowViewModel or MainWindow directly?If it is binded to MainWindow how do you get it to bind to a singleton.If it was another class it just created I could understand, but I assume you have to use code behind?
  • JonWillis
    JonWillis over 12 years
    Do you use a publicly available framework/set of classes for MVVM. I have seen in some other tutorials some pre-written ViewModelBase classes. Following the "In the Box Tutorial" they have some help methods instead of abstract classes, probably to keep it simple. Can you expand a bit more on the ViewModelConverter. As I would like to try creating error pages in future revisions of this test, i.e. A link they click in an overview page tries to open an object with id 334, which doesn't exist.
  • JonWillis
    JonWillis over 12 years
    Your design on showing the overview screen, through the PageManager, and then showing the login screen instead due to the CurrentUser == null is what I would consider to be ideal for what I am trying to achieve through this experiment. If you could provide more specific details on this, that would be great. 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.
  • BoltClock
    BoltClock over 12 years
    Holy long answer, Robin! (... I know Batman doesn't say "holy".)
  • JonWillis
    JonWillis over 12 years
    I'm still digesting the new information, but have marked the question as answered as you have gone over and above the content of the original question with an indepth answer. I really like the idea of converting a viewmodel into a view. And wiring it up by Setting vm.PageTemplate to the view, and the view.DataContext to the viewmodel. Ill go through more mvvm tutorials and see what templates I can use as a viewmodelbase then like you modify it overtime to meet my needs. I don't want to risk trying to tackle to many WPF/MVVM concepts at once and end up forgetting rather than learning.