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.
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
}
}
Holy long question, Batman!
Q1: The process would work, I don't know about using the
LoginModel
to talk to theMainWindowViewModel
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 theLoginViewModel
or the Singleton (Personally, I would probably implement this on theViewModel
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
orViewModelManager
. 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 virtualOnLoaded(), OnDisplayed() and OnClosed()
methods that can be defined in theViewModel
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 eventuallyGC
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 theLoginView
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
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())
(orPageManager.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 aLogin(string, string)
method, which sets the CurrentUser on successful log in.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.Yes. I use this class as the base class of all my ViewModel's.
This class contains
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.
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.
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:
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.
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.Sure, to give you an outline it would be easier to show some code.
Reading through this code in sections it reads:
return the template. This will then be displayed in a ContentPresenter for the user to see.
This is the code in the converter that does most of the grunt work, reading through the sections you can see:
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.