Click here to Skip to main content
13,625,905 members
Click here to Skip to main content
Add your own
alternative version

Stats

7.6K views
128 downloads
21 bookmarked
Posted 5 Jan 2017
Licenced CPOL

Bare Metal MVVM - Where The Code Meets The Road - Part 2

, 5 Jan 2017
Rate this:
Please Sign up or sign in to vote.
This is part 2 in a series of articles which covers MVVM from the absolute beginning; no frameworks and no helpers, we're going to learn MVVM from the nuts and bolts

Introduction

There have been many, many articles about MVVM so you may wonder why I am writing more articles on it. The simple reason is, there's a lot of misinformation about MVVM out there and I hope that this series of articles helps to dispel some of the myths about what is a really, really straightforward pattern.

Now, you are probably aware that MVVM started off life as a pattern for WPF applications, and that it has spread out from its humble beginnings into other languages, frameworks and platforms. However, because these different implementations all rely on features that aren't visibly exposed to us, the developer, we tend to miss out on just what it is that makes MVVM such a useful tool in our development toolbox, and what features we need to start off with MVVM. So, for this article (as we did in the first one), we're going to ditch all those fancy platforms, and build an MVVM application from the ground up as a Console application. Yes, that's right, we're going to recreate some of what goes on behind the scenes to show some of the "magic" that makes MVVM possible.

In this article, we're going to look at making our binding code handle more complex scenarios, as well as looking at how to improve our application by supporting menus and handling commands based off user input.

Series

Development Environment

I have developed this application in VS 2015 and made liberal use of C#6 features. If you don't have VS2015 installed, I hope you will at least be able to follow along with the concepts as I will explain the entire codebase here.

Introducing complexity

As we discussed in the last article, we were confining ourselves to dealing with a simple View to ViewModel to Model interaction, with the binding engine only picking up a very straightforward direct binding, and requiring the DataContext to be set before the binding is established. It's now time to address these deficiencies. The first thing we are going to do is define a new Model that exposes a hardcoded integer - as of today, everyone is now 32 years old. So, this model looks like this.

public class AgeModel
{
  public int Age => 32;
}

Now, we need to update the PersonViewModel to expose this model. We aren't going to expose the Age value to the View, instead we're going to allow the View to bind via the ViewModel to the Age inside our AgeModel (I know, it would appear, at this stage, that the View shouldn't have knowledge about the Model, but this deep binding facility is particularly useful when you have parent/child ViewModels).

public class PersonViewModel : INotifyPropertyChanged
{
  private readonly PersonModel personModel;
  private readonly PersonModelValidator validator;
  private readonly AgeModel age;

  public PersonViewModel()
  {
    personModel = new PersonModel();
    validator = new PersonModelValidator();
    age = new AgeModel();
  }

  public AgeModel Age
  {
    get { return age; }
  }

  public string Name
  {
    get { return personModel.Name; }
    set
    {
      if (!validator.IsValid(value))
      {
        return;
      }
      personModel.Name = value;
      OnPropertyChanged();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

 We see here that we are exposing the AgeModel, rather than age.Age, so we need to add some fall through binding into the View (Age.Age). 

public class ProgramView : Framework
{
  private readonly PersonViewModel viewModel = new PersonViewModel();
  public ProgramView()
  {
    DataContext = viewModel;
    SetBinding("Name");
    SetBinding("Age.Age");
  }

  public void Input()
  {
    string input = Console.ReadLine();
    viewModel.Name = input;
  }
}

If we run the application right now, we'll see that we don't get the output we would expect.

Broken application - INPC fired twice.

Why is this? Why did typing this value once update the property twice? What could be happening here? The answer lies in the rather naive implementation of the Parse method that we covered in the last article. That code hooked up to the INPC regardless as to whether or not we had already bound to that instance. Okay, let's fix that particular defect.

private static readonly HashSetinstances = new HashSet(); 
public void Parse() 
{
 INotifyPropertyChanged inpc = Source as INotifyPropertyChanged;
 if (inpc == null)
 {
   return;
 }
 if (instances.Contains(inpc))
 {
   return;
 }
 instances.Add(inpc);
 inpc.PropertyChanged += Inpc_PropertyChanged; 
}

We're going to maintain a list of instances that we have already assigned the INPC handler against. As each binding is a new class, we have a static list of the instances we have assigned the INPC to. Inside the Parse method, we need to check to see whether or not we have already assigned the handler here and break out if we have. Now if we run this, we should see that the INPC code runs the expected number of times.

The INPC code only fires once now.

This is much better but we're still missing something. We don't see the initial values, so we haven't seen the Age value here. Let's correct that now, and display the values we would expect to see. Fortunately for us, retrieving arbitrarily complex trees is a fairly straightforward process. For the moment, all we need to do is add a method that will "walk down" the bindings until it can display the property. Let's add this method to our codebase.

private static object Show(object type, string propertyName)
{
  while (true)
  {
    string[] properties = propertyName.Split('.');
    PropertyInfo propertyInfo;
    if (properties.Length > 1)
    {
      string property = properties.First();
      propertyName = propertyName.Substring(property.Length + 1);
      propertyInfo = type.GetType().GetProperty(propertyName);
      if (propertyInfo == null)
      {
        return $"Data binding error: Unable to get {propertyName} from {type.GetType().FullName}";
      }
      type = propertyInfo.GetValue(type);
      continue;
    }
    propertyInfo = type.GetType().GetProperty(propertyName);
    if (propertyInfo == null)
    {
      return $"Data binding error: Unable to get {propertyName} from {type.GetType().FullName}";
    }
    object value = propertyInfo.GetValue(type);
    return value ?? "<<Unset>>";
  }
}

That should, quite neatly, walk the hierarchy, ready to display the value that needs to be displayed. It's important to understand, at this point, that this doesn't cope with lists. Once we introduce collection support into our framework, we will have to revisit this functionality. For the moment, as we have a simple object hierarchy, this is sufficient for our needs. Now all we need to do is call this method and display the values. What is interesting is the code that caters for the property not being present. As the binding is just entered as a string, it's entirely possible that we could type the property name incorrectly. By returning a message that the binding has not been set, we have a good indication that a binding has been entered incorrectly. Right, it's time to revisit the Parse method one last time and amend it to show the values.

public void Parse()
{
  object value = Show(Source, Name);
  Console.WriteLine($"{Name} is {value}");
  INotifyPropertyChanged inpc = Source as INotifyPropertyChanged;
  if (inpc == null)
  {
    return;
  }
  if (instances.Contains(inpc))
  {
    return;
  }
  instances.Add(inpc);
  inpc.PropertyChanged += Inpc_PropertyChanged;
}

There. We have something that successfully displays the values from a binding. Now, when we run our application, it should look like this:

Application running

Making things interesting

I have to admit that this particular example application feels a little bit underwhelming right now because we follow a proscribed route where the user just enters a name, and that's it the program exits. Wouldn't it be great, if our application was a bit more interactive, allowing the user to choose options from a menu? We are going to change things up a bit so that we let the users change the background and foreground colors of the console, as well as allowing them to enter the name or quit the application. This is what our application could look like if the user decides to change from the default color scheme.

Application running with user choosing options.

Now that we know what we're going to build, the fist thing we need to do is add in a little bit of infrastructure for the menu entries. We're going to start off with a relatively straightforward couple of classes, one to represent an individual menu entry and a menu class that groups these individual entries together.

public class MenuItem
{
  public string Header { get; set; }
  public ICommand Command { get; set; }
}

The MenuItem represents a single entry that we want to display on the screen, and take some action when the user selects it. We can see that we have an ICommand entry in there, representing the command that will be executed when the user chooses that particular option. We'll come back to this particular interface very shortly but we're going to finish off by fleshing out our Menu class.

public class Menu
{
  private readonly List<MenuItem> menuItems = new List<MenuItem>();
  public void Add(MenuItem menuItem)
  {
    menuItems.Add(menuItem);
  }

  public IEnumerable<MenuItem> Items => menuItems;
}

That's a pretty straightforward implementation. This class allows us to group a number of menu item entries together, which we will use when we start adding our menu entries in.

ICommand

In the previous article, I did promise that we would deal with support for adding commands and that's what we're going to do now. For those who are familiar with WPF, the ICommand interface should be very familiar. Well, it would be if we were going to use the one that WPF provides, which is overkill for our purposes. Instead, we're going to implement our own simplified ICommand interface that just exposes the Execute method from the WPF version. The point here is, while WPF provides its own command infrastructure, other platforms and frameworks provide different capabilities and implementations, so we're going to start off with one that allows us to explore the rationale behind the commanding. When we come to looking into MVVM in WPF later on in the series, we'll have to think about the command management infrastructure in WPF, which is a touch heavy on the theory for the moment. Anyway, here's our ICommand interface.

public interface ICommand
{
  void Execute();
}

All the interface consists of is a contract that specifies that we will execute the action that the user wants to perform using the Execute method. So, now that we have the command interface defined, let's actually put an implementation behind this. Now, we're going to base this implementation on Josh Smith's RelayCommand implementation that he wrote for WPF, many years ago. The reason we're going to base it off his version is that there are some subtle things in his implementation that we're going to address when we look at WPF MVVM so it seemed appropriate to use his version, and we'll tackle why there are implementations such as Prism's DelegateCommand.

public class RelayCommand : ICommand
{
  private readonly Action executeAction;
  public RelayCommand(Action execute)
  {
    executeAction = execute;
  }
  public void Execute()
  {
    executeAction();
  }
}

That couldn't be any simpler. When we create an instance of this class, we pass in an Action that we want the command to perform and when we call Execute, it executes the action. Okay, it's time for us to revisit our ViewModel. We're going to revamp it heavily.

public class PersonViewModel : INotifyPropertyChanged
{
  private readonly PersonModel personModel;
  private readonly PersonModelValidator validator;

  public PersonViewModel()
  {
    personModel = new PersonModel();
    validator = new PersonModelValidator();
    Age = new AgeModel();
    Menu.Add(GetMenuItem("1. Change the name", InputName));
    Menu.Add(GetMenuItem("2. Change the foreground color to red", ()=> SetForegroundColor(ConsoleColor.DarkRed)));
    Menu.Add(GetMenuItem("3. Change the foreground color to green", () => SetForegroundColor(ConsoleColor.DarkGreen)));
    Menu.Add(GetMenuItem("4. Change the foreground color to white", () => SetForegroundColor(ConsoleColor.White)));
    Menu.Add(GetMenuItem("5. Change the background color to cyan", () => SetBackgroundColor(ConsoleColor.DarkCyan)));
    Menu.Add(GetMenuItem("6. Change the background color to yellow", () => SetBackgroundColor(ConsoleColor.DarkYellow)));
    Menu.Add(GetMenuItem("7. Change the background color to black", () => SetBackgroundColor(ConsoleColor.Black)));
    Menu.Add(GetMenuItem("q. Quit", () => Environment.Exit(0)));
  }

  private void SetForegroundColor(ConsoleColor consoleColor)
  {
    Console.ForegroundColor = consoleColor;
    PrintMenu(true);
  }

  private void SetBackgroundColor(ConsoleColor consoleColor)
  {
    Console.BackgroundColor = consoleColor;
    PrintMenu(true);
  }

  public Menu Menu { get; } = new Menu();

  public void PrintMenu(bool clearScreen)
  {
    if (clearScreen)
    {
      Console.Clear();
    }
    foreach (MenuItem menuItem in Menu.Items)
    {
      Console.WriteLine(menuItem.Header);
    }
  }

  public AgeModel Age { get; }

  public string Name
  {
    get { return personModel.Name; }
    set
    {
      if (!validator.IsValid(value))
      {
        return;
      }
      personModel.Name = value;
      OnPropertyChanged();
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }

  private void InputName()
  {
    Console.WriteLine($"{Environment.NewLine}Please enter a name");
    string input = Console.ReadLine();
    PrintMenu(true);
    Name = input;
  }

  private static MenuItem GetMenuItem(string header, Action commandAction)
  {
    return new MenuItem { Header = header, Command = new RelayCommand(commandAction) };
  }
}

I appreciate that there's a lot going on in there, so let's break this down into smaller chunks. We're going to introduce a new property here that we're going to bind to from our View. This property is the Menu that we're going to populate in the constructor like this:

public PersonViewModel()
{
  personModel = new PersonModel();
  validator = new PersonModelValidator();
  Age = new AgeModel();
  Menu.Add(GetMenuItem("1. Change the name", InputName));
  Menu.Add(GetMenuItem("2. Change the foreground color to red", ()=> SetForegroundColor(ConsoleColor.DarkRed)));
  Menu.Add(GetMenuItem("3. Change the foreground color to green", () => SetForegroundColor(ConsoleColor.DarkGreen)));
  Menu.Add(GetMenuItem("4. Change the foreground color to white", () => SetForegroundColor(ConsoleColor.White)));
  Menu.Add(GetMenuItem("5. Change the background color to cyan", () => SetBackgroundColor(ConsoleColor.DarkCyan)));
  Menu.Add(GetMenuItem("6. Change the background color to yellow", () => SetBackgroundColor(ConsoleColor.DarkYellow)));
  Menu.Add(GetMenuItem("7. Change the background color to black", () => SetBackgroundColor(ConsoleColor.Black)));
  Menu.Add(GetMenuItem("q. Quit", () => Environment.Exit(0)));
}

GetMenuItem simply builds and returns a new instance of a MenuItem that we can add into our Menu. The Action that is passed in to this method is the one we use to build our RelayCommand. Apart from Environment.Exit, the other actions all relate to code we're going to have to write. InputName is going to allow the user to enter a new name while SetForegroundColor will change the color of the foreground and SetBackgroundColor, cunningly enough, changes the background color.

private static MenuItem GetMenuItem(string header, Action commandAction)
{
  return new MenuItem { Header = header, Command = new RelayCommand(commandAction) };
}

private void InputName()
{
  Console.WriteLine($"{Environment.NewLine}Please enter a name");
  string input = Console.ReadLine();
  PrintMenu(true);
  Name = input;
}

private void SetForegroundColor(ConsoleColor consoleColor)
{
  Console.ForegroundColor = consoleColor;
  PrintMenu(true);
}

private void SetBackgroundColor(ConsoleColor consoleColor)
{
  Console.BackgroundColor = consoleColor;
  PrintMenu(true);
}

We see a liberal scattering of calls to PrintMenu in here. When we call this and pass in true as the parameter, we are going to clear the screen and redisplay the menu text. We do this so that we always have a clean view of the menu for the user - well, as clean as some of the more garish color combinations provide. The reason that we call PrintMenu before we set the Name value in InputName is because we want to ensure that we show the change of the name to the user. This all feels very clunky, and it is. In future installments, we'll cover the concepts of templates and see how we can apply templates to make things a lot cleaner here.

public void PrintMenu(bool clearScreen)
{
  if (clearScreen)
  {
    Console.Clear();
  }
  foreach (MenuItem menuItem in Menu.Items)
  {
    Console.WriteLine(menuItem.Header);
  }
}

By the way, the reason that this is a public method is because we're going to call this directly from the View as well, to print the menu out initially - we do that because we still want to see the initial values. This is something that we will fix when we get to adding template support - for now, I'm happy to leave this as it is.

Speaking of the View, what changes do we need to do in there? Well, we're just going to add two things (for the moment - we're shortly going to look at a change to Framework that affects the structure of the View slightly). The first is to bind to the Menu entry in the ViewModel and the other is to print the initial menu.

SetBinding("Menu");
viewModel.PrintMenu(false);

But how does this work? How does the menu binding actually work as we don't have any special code in place to handle this just yet? Well, in order to support this, we need to beef up the Binding class a touch. Basically, we need to do two things in there, the first is to provide a Menu property that exposes our Menu and the second is to modify the Parse method to check the entry so that, when the value of the bind is a Menu, it adds to our new property. There is a limitation in this code in that the Menu cannot be a nested property. Again, that's something we will address later on when we provide template support for our application. By the way, the reason we have provided a Menu on the binding is that we want, at this stage, to allow our code to have multiple menus if we need. This obviously is a fairly contrived example but it's not one that we need be too worried about yet.

public Menu Menu { get; private set; }
public void Parse()
{
  object value = Show(Source, Name);
  Menu menu = value as Menu;
  if (menu != null)
  {
    Menu = menu;
    return;
  }
  Console.WriteLine($"{Name} is {value}");
  INotifyPropertyChanged inpc = Source as INotifyPropertyChanged;
  if (inpc == null)
  {
    return;
  }
  if (instances.Contains(inpc))
  {
    return;
  }
  instances.Add(inpc);
  inpc.PropertyChanged += Inpc_PropertyChanged;
}

The last piece of the puzzle is how are we going to automatically hook up the menus so that the system waits for an appropriate keypress and then acts on the input? For this, we're going to have to modify the Framework class a little bit to provide an input loop and hook this back to our View. What we're going to do is add a virtual Initialize method that we will override in our View, which is where we will set up our bindings. The last thing this virtual method will do is call our input loop.

protected virtual void Initialize()
{
  InputLoop();
}

private void InputLoop()
{
  while (true)
  {
    ConsoleKeyInfo key = Console.ReadKey();
    foreach (MenuItem menuItem in bindings.Values.Where(binding => binding.Menu != null)
      .SelectMany(binding => binding.Menu.Items.Where(
        menuItem => menuItem.Header.Substring(0, 1) == key.KeyChar.ToString())))
    {
      menuItem.Command.Execute();
    }
  }
}

The only vaguely scary part of this code is the LINQ statement in the foreach. This is just iterating over all the none null menu entries in the bindings, and then looking for entries where the first character in the menu header matches the key the user pressed. When it finds a match, it's going to execute the command associated with that menu item. The last piece we need to slot into place now is the amendment to our View to handle the Initialize method.

public class ProgramView : Framework
{
  private readonly PersonViewModel viewModel = new PersonViewModel();

  protected override void Initialize()
  {
    DataContext = viewModel;
    SetBinding("Name");
    SetBinding("Age.Age");
    // Well, this is different. We're binding to the Menu here.
    SetBinding("Menu");
    viewModel.PrintMenu(false);

    base.Initialize();
  }
}

Wrapping up

Well that's it. We have enhanced our Console framework to support deeper binding structures, and to handle commands. Command handling plays a large part in MVVM applications, so it's something we needed to address before we go any further with this series. In the next article, we're going to start looking at how we can provide template support for our application, and see how we can use this to better affect the way we bind things together. We will also start to investigate the wonderful world of collection support.

 

 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Pete O'Hanlon
CEO
United Kingdom United Kingdom
A developer for over 30 years, I've been lucky enough to write articles and applications for Code Project as well as the Intel Ultimate Coder - Going Perceptual challenge. I live in the North East of England with 2 wonderful daughters and a wonderful wife.

I am not the Stig, but I do wish I had Lotus Tuned Suspension.

You may also be interested in...

Comments and Discussions

 
QuestionInputName() in VM Pin
bakunet19-Apr-18 22:08
memberbakunet19-Apr-18 22:08 
QuestionMissing code... Pin
bakunet19-Apr-18 20:56
memberbakunet19-Apr-18 20:56 
BugSmall Error in Recursive Property Reflection Pin
mldisibio1-May-17 10:46
membermldisibio1-May-17 10:46 
PraiseAnother Great Article Pin
ronlease11-Jan-17 5:03
professionalronlease11-Jan-17 5:03 
GeneralRe: Another Great Article Pin
Pete O'Hanlon11-Jan-17 5:28
protectorPete O'Hanlon11-Jan-17 5:28 
PraiseRe: Another Great Article Pin
ronlease16-Jan-17 8:10
professionalronlease16-Jan-17 8:10 
GeneralRe: Another Great Article Pin
Pete O'Hanlon23-Feb-17 23:49
protectorPete O'Hanlon23-Feb-17 23:49 
QuestionLiking this Pin
Sacha Barber6-Jan-17 6:10
mvpSacha Barber6-Jan-17 6:10 
AnswerRe: Liking this Pin
Pete O'Hanlon6-Jan-17 6:57
protectorPete O'Hanlon6-Jan-17 6:57 
AnswerRe: Liking this Pin
Pete O'Hanlon23-Feb-17 23:50
protectorPete O'Hanlon23-Feb-17 23:50 
GeneralExcellent second part Pin
Daniel Vaughan5-Jan-17 11:51
memberDaniel Vaughan5-Jan-17 11:51 
GeneralRe: Excellent second part Pin
Pete O'Hanlon5-Jan-17 21:25
protectorPete O'Hanlon5-Jan-17 21:25 
GeneralRe: Excellent second part Pin
Member 113571435-Jan-17 22:23
memberMember 113571435-Jan-17 22:23 
GeneralRe: Excellent second part Pin
Pete O'Hanlon5-Jan-17 22:50
protectorPete O'Hanlon5-Jan-17 22:50 
GeneralRe: Excellent second part Pin
Pete O'Hanlon23-Feb-17 23:50
protectorPete O'Hanlon23-Feb-17 23:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.180712.1 | Last Updated 5 Jan 2017
Article Copyright 2017 by Pete O'Hanlon
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid