Subscribe Now: Feed Icon

Wednesday, March 23, 2011

Starting with MVVM (in Silverlight)

For the past two weeks I have been working on both a new feature and converting the existing project to work with MVVM. Until now the XAML had code behind to handle all of the functionality and even for some stuff like combo box items we used code behind (where data binding would have been a lot easier).

I decided to spend some time and start converting the project. One of my teammates advised me to check out the MVVM Light framework. He even pointed me towards a good presentation in MIX 2010 on this subject.

So where should you start?

I found the best place to start is in a control/window that has simple actions. It is easy to just do the data binding without any care and a great stepping stone for other types of controls.

For example lets implement the classic – CalcControl. The control is just going to add two numbers when the Add Button is clicked.

The MVVM class:

  1. public class CalcViewModel:ViewModelBase
  2.  {
  3.      public RelayCommand AddCommand
  4.      {
  5.          get;
  6.          private set;
  7.      }
  8.  
  9.      public CalcViewModel()
  10.      {
  11.          FirstNumber = 0;
  12.          SecondNumber = 0;
  13.  
  14.          AddCommand = new RelayCommand(Add);
  15.      }
  16.  
  17.      private void Add()
  18.      {
  19.          Result = FirstNumber + SecondNumber;
  20.      }
  21.  
  22.      private const string FirstNumberPropertyName = "FirstNumber";
  23.      private int _firstNumber;
  24.  
  25.      public int FirstNumber
  26.      {
  27.          get { return _firstNumber; }
  28.          set
  29.          {
  30.              _firstNumber = value;
  31.              RaisePropertyChanged(FirstNumberPropertyName);
  32.          }
  33.      }
  34.  
  35.      private const string SecondNumberPropertyName = "SecondNumber";
  36.      private int _secondNumber;
  37.  
  38.      public int SecondNumber
  39.      {
  40.          get { return _secondNumber; }
  41.          set
  42.          {
  43.              _secondNumber = value;
  44.              RaisePropertyChanged(SecondNumberPropertyName);
  45.          }
  46.      }
  47.  
  48.      private const string ResultPropertyName = "Result";
  49.      private int _result;
  50.  
  51.      public int Result
  52.      {
  53.          get { return _result; }
  54.          set
  55.          {
  56.              _result = value;
  57.              RaisePropertyChanged(ResultPropertyName);
  58.          }
  59.      }
  60.  }

The Control:

  1. <UserControl x:Class="MyControl.CalcControl"
  2.     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.     xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  5.     xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  6.     mc:Ignorable="d"
  7.     d:DesignHeight="300" d:DesignWidth="400" xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/toolkit">
  8.     
  9.     <Grid x:Name="LayoutRoot" Background="White">
  10.         <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
  11.             <TextBlock Text="First Number"/>
  12.             <toolkit:NumericUpDown Height="22" Value="{Binding FirstNumber, Mode=TwoWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="88" />
  13.             <TextBlock Text="Second Number"/>
  14.             <toolkit:NumericUpDown Height="22" Value="{Binding SecondNumber, Mode=TwoWay}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="88" />
  15.             <Button Content="Add" Command="{Binding AddCommand, Mode=OneWay}" Width="88" />
  16.             <TextBlock Text="{Binding Result, Mode=OneWay}"/>
  17.         </StackPanel>
  18.     </Grid>
  19. </UserControl>

(Just binding the values TwoWay – important, the result – OneWay, and binding the Button to the command)

Using it is as simple as:

  1. <MyControl:CalcControl x:Name="calc" DataContext="{Binding CalcData}"/>

(in your page where CalcData is a property inside the MVVM class for the page)

Or if you are still using code behind:

  1. calc.DataContext = new CalcViewModel();

The result (be warned it’s not pretty but it works):

Simple-Silverlight-mvvm-control

 

After finishing those types of controls the next step is making ground for the big guns => Tweaking the MainPage. I am sure many of you are shouting “How is that simple? Do you know how much code I have there?”. Well 1. this is a really small change and 2. I have ~700 lines of code in the MainPage at this moment (so just wait).

As I said we are going to do a minor change – adding a BusyIndicator control that wraps around the MainPage. The BusyIndicator control (for those who don’t know) is a control that makes it easy for the user to know when the application is busy by making all the controls in it readonly.

We will use the control with it’s two properties of IsBusy and BusyContent (you can use it with custom busy content as well).

We are going to implement something like this:

 

The end result is this:

BusyIndicator-example

The Locator class:

  1. public class ViewModelLocator
  2. {
  3.  
  4.     public static IUnityContainer Container
  5.     {
  6.         get;
  7.         private set;
  8.     }
  9.  
  10.     static ViewModelLocator()
  11.     {
  12.         Container = new UnityContainer();
  13.  
  14.         Container.RegisterType<MainViewModel>(new ContainerControlledLifetimeManager());
  15.     }
  16.  
  17.     public MainViewModel Main
  18.     {
  19.         get
  20.         {
  21.             return Container.Resolve<MainViewModel>();
  22.         }
  23.     }
  24.     
  25.     public static void Cleanup()
  26.     {
  27.         Container.Resolve<MainViewModel>().Cleanup();
  28.     }
  29. }

(this class will be used to bind the MainViewModel in the MainPage since it’s the page that is loaded automatically, you can also add in the constructor a dummy view model in design time by using:

  1. //if (ViewModelBase.IsInDesignModeStatic)
  2. //{
  3. //    Container.RegisterType<IDataService, Design.DesignDataService>();
  4. //}
  5. //else
  6. //{
  7. //    Container.RegisterType<IDataService, DataService>();
  8. //}

which was taken from the MIX10 sample source code. I haven’t yet implemented it…)

BusyMessage.cs:

  1.     public BusyMessage(bool isBusy, BusyReason reason)
  2.     {
  3.         IsBusy = isBusy;
  4.         Reason = reason;
  5.     }
  6.  
  7.     public bool IsBusy { get; set; }
  8.     public BusyReason Reason { get; set; }
  9. }
  10.  
  11. [Flags]
  12. public enum BusyReason
  13. {
  14.     NotBusy = 0,
  15.     JustFeelLikeIt = 1,
  16.     JustBecause = 2,
  17.     DoIRealyNeedAnotherReason = 4
  18. }

(the BusyMessage contains two properties the IsBusy and the Reason for being busy, Reason is a flag enum that allows you to add several reasons together)

MainViewModel.cs:

  1. public class MainViewModel : ViewModelBase
  2. {
  3.     private const string DefaultBusyMessage = "Busy...";
  4.  
  5.     public MainViewModel()
  6.     {
  7.         IsBusy = false;
  8.         BusyMessage = DefaultBusyMessage;
  9.         BusyReason = BusyReason.NotBusy;
  10.  
  11.  
  12.         Messenger.Default.Register<BusyMessage>(
  13.             this,
  14.             m => HandleBusyMessage(m.IsBusy, m.Reason));
  15.     }

(Pretty straight forward setting the default values and registering for the BusyMessage)

  1. private string GetBusyMessage()
  2. {
  3.     if (BusyReason == BusyReason.NotBusy)
  4.         return DefaultBusyMessage;
  5.     return StringUtils.EnumToSentence(BusyReason);
  6. }

(This method just converts the Enum to a readable sentence, I using another generic method that converts the enum to a sentence by first converting it to a string and then converts Pascal Case to regular text)

  1. private void HandleBusyMessage(bool isBusy, BusyReason reason)
  2. {
  3.     if (isBusy)
  4.     {
  5.         BusyReason |= reason;
  6.         if(!IsBusy)
  7.         {
  8.             BusyMessage = GetBusyMessage();
  9.             IsBusy = true;
  10.         }
  11.     }
  12.     else
  13.     {
  14.         BusyReason ^= reason;
  15.         if (BusyReason == BusyReason.NotBusy)
  16.             IsBusy = false;
  17.         BusyMessage = GetBusyMessage();
  18.     }
  19. }

(This method handles the message by using the BusyReason Flags attribute (|= adds a flag, ^= removes a flag))

  1. protected BusyReason BusyReason { get; set; }
  2.  
  3. #region MVVM Properties
  4.  
  5. private const string IsBusyPropertyName = "IsBusy";
  6. private bool _isBusy;
  7.  
  8. public bool IsBusy
  9. {
  10.     get { return _isBusy; }
  11.     set
  12.     {
  13.         _isBusy = value;
  14.         RaisePropertyChanged(IsBusyPropertyName);
  15.     }
  16. }
  17.  
  18. private const string BusyMessagePropertyName = "BusyMessage";
  19. private string _busyMessage;
  20.  
  21. public string BusyMessage
  22. {
  23.     get { return _busyMessage; }
  24.     set
  25.     {
  26.         _busyMessage = value;
  27.         RaisePropertyChanged(BusyMessagePropertyName);
  28.     }
  29. }
  30.  
  31. #endregion

(The properties being used, BusyReason is not used outside this class)

 

MainPage.XAML:

  1. <UserControl.DataContext>
  2.     <Binding Mode="OneWay" Path="Main" Source="{StaticResource Locator}"/>
  3. </UserControl.DataContext>

(this just bind the MainViewModel to our page since it is the first page being loaded)

  1. <controls:BusyIndicator BusyContent="{Binding Path=BusyMessage}" IsBusy="{Binding Path=IsBusy}">

(and this is the bound BusyIndicator control)

 

Now using it is as easy as:

  1. Messenger.Default.Send(new BusyMessage(true, BusyReason.JustFeelLikeIt));

(to activate the BusyIndicator with a message of Just feel like it)

And:

  1. Messenger.Default.Send(new BusyMessage(false, BusyReason.JustFeelLikeIt));

(to deactivate the BusyIndicator with a message of Just feel like it)

And again the end result is this:

BusyIndicator-example

So, what do you think?

 

One last advice: watch the classes where you use Messenger.Default.Register because you will have to clean those classes. For example I had used it in a control to draw on a map when entering edit mode but forgot to clean it up, the next thing I know the drawings was done several times. Another drawback to not cleaning it up is in a possible memory leak since even after not using the class it is still being referred in the Messenger. To clean a whole class use:

  1. Cleanup();

To removed a certain message in the current class:

  1. Messenger.Default.Unregister<BusyMessage>(this);

(if you need to remove it from another class just replace the “this”)

 

After finishing this example almost everything else is quite easy. The one difficult thing I did encounter was switching the AutoCompleteBox to MVVM but I found a blog post or two that describes the process (we used the Populating event). And I hope it’s going to help me along… I will let you know how it goes later on.

 

Keywords: Silverlight, MVVM

IceRocket Tags: ,