Click here to Skip to main content
15,881,882 members
Articles / Desktop Programming / WPF

MvvmCrudGv - (Roll you own Wpf Mvvm Framework CRUD application – hands on in 14 (or so) steps)

Rate me:
Please Sign up or sign in to vote.
4.91/5 (22 votes)
2 Nov 2014CPOL19 min read 70.2K   3.5K   36   22
Yet another Basic .Net Wpf Mvvm Framework CRUD application – hands on in 14 (or so) easy steps for WPF/MVVM beginners / for practice.

Introduction

The source code for this application is also available on GitHub here:-

The Service in this application is WCF compatible. Below is the link to the self Hosted WCF version of this project (This version self hosts the TodoService in a Thread and consumes it in the Application. Rest of the functionality is all the same. ):-

 

MvvmCrudGv is a basic CRUD app (todo lis application) written in .Net wpf MVVM pattern.  Below is how the App. looks:-

Image 1Image 2


What we have here is the first CRUD page displaying List of Todo’s, we can perform basic crud editing on the items.

  • New button adds a new Item to the list which we can Edit and Save.
  • We can click to select an Item in the ListView and Edit/Save/Delete it.
  • We can go to the details page using “Details” button and do the editing for selected item. 
  • We can also double click an item in list to go to details page.


This application basically uses a very simple CRUD Database implementation based on Protobuf. Here we simply use it for persistence, for further details about protobuf serialization we can go to [this link].

Background

There are a number of WPF MVVM frameworks available. However, the number of features in each might get overwhelming for a beginner to start at. So the following basic 14 step framework should be able to give us a kick start. Then we can move ahead to our journey to more elaborate frameworks.

How we created the App:

Below are the steps we performed to create this project. Code listings for the steps are outlined below. Some of the explainatrion about code is listed below next to the code where I thought it would be more easier to understand while  looking at the code itself:-

(Objective 1: Setup Basic Project Structure)   

  1. Create a wpf project in Visual Studio we will call it MvvmCrudGv. (lots of quickstarts available online). (Listing 1a)
    • Add another project type ClassLibrary to this solution, call it MvvmCrudGv.Service. Add reference System.ServiceModel, System.Runtime.Serialization to service. -(We will add our Persistence service here. We can write our own Data layer from here onwards to persist the service objects.)
  2. Create these folders -
    • We added folders ViewModels, Views,Common, Common>Behaviors, Common>Messaging (Viewmodels store our viewmodels, common stores common files, View stores views)
    • We added folders Entity, Persistence in MvvmCrudGv.Service (Listing 2a)
    • Add a our simple Todo (our basic DataContract which we want to persist) to MvvmCrudGv.Service.Entity. (Listing 2b)
    • In MainWindow.xaml.cs we create a dummy list of these Todo's and bind to this dummy list in MainWindow.xaml to test it. (Listing 2c)
    • Test the MainWindow's is binding to DataContext (TodoList). (Listing 2d)
  3. Move MainWindow to Views folder. In App.xaml point the startup uri to correct place (StartupUri=Views/MainWindow.xaml). Test it is working.
  4. Add a class ViewModels\MainWindowViewmodel. We want our MainWindow.xaml to use this class as its ViewModel/DataContext. (Listing 4a)
  5. Add Common\ViewModelLocator. This will be the common class which will define our View=>ViewModel routings.  (Listing 5a)
    • This is our injector/mapper of Views=>ViewModels.
    • Expose public property type MainWindowViewModel. (Listing 5b)
    • We will put it in a common place in "App.xaml" resource dictionary so that anybody can make use of this class anytime.
    • In App.xaml add the ResourceDictionary (Listing 5c)
    • Bind the Views/MainWindow.xaml to ViewModels\MainWindowViewModel (Listing 5d)
      • In Views/MainWindow.xaml bind the property DataContext="{Binding MainWindowViewModel, Source={StaticResource Locator}}"
      • We will see the display of list will disappear because the DataContext has changed and we should move our bindings to its ViewModel now. (Listing 5e)
        • Remove the TodoList logic from MainWindow.xaml.cs and move it to MainWindowViewModel.cs. (Listing 5f) Set the ListView ItemsSource="{Binding TodoList}". Test that view is displaying the list correctly now.. 

    • Milestone 1: Basic Structure Setup Done.

      (Objective 2: Setup communication platform between Application objects)
  6. Refractor to include navigation between pages. This probably is the most important function of our App.
    • Add IEventAggregator and EventAggregator Classes. More information below (Listing 6a).
    • Add the NavMessage class. This is a class which can be used to publish/subscribe a Navigation event. (Listing 6b)
    • Below is the basic idea of how the navigation message in our system works (open the below image in new window for clearer/full view). For more details about components please check the Listing 6a and Listing 6b as mentioned in steps above. :-
    • Image 3

      Milestone 2: Mvvm Navigation ready.

      (Objective 3: Add Service Layer and bind it to View.)
  7. Our EventAggregator or messaging system is ready. Now there is time to use it.
    • Create a new Views Home.xaml, TodoDetails.xaml and HomeViewModel, TodoViewModel. The TodoDetails view we will use later. First of all let us use Home view. Our MainWindow will straight away navigate to Home view once loaded. Let us create the frame and hook the navigation code to our MainWindow. (Listing 7c)
    • Move the view code from MainWindow.xaml to Home.xaml. Move the code from MainWindowViewModel to HomeViewModel. (Listing 7d)
    • Our MainWindow.xaml will have a menu strip at top and a Frame Name=MainFrame below it. Let us add the code. (Listing 7e)
    • Create the properties for HomeViewModel and TodoViewModel in ViewModelLocator. Bind Home.xaml and TodoDetails.xaml to their ViewModels. (Listing 7f) (Listing 7g)
    • In App.xaml.cs add a static IEventAggregator. On startup of application instantiate this eventAggregator. (Listing 7h)
    • In the MainWindow.xaml.cs use this App.eventAggregator to subscribe to events of type NavMessage, and perform navigation on the MainFrame according to NavMessage. (Listing 7i)
  8. Our framework is now actually ready for our CRUD operations. Let us write the actual Service and Persistence logic now. To Project MvvmCrudGv.Service add the ServiceContract and its implementation. (ITodoService and TodoService resp) (Listing 8a)
  9. Add a singleton Common\BootStrapper. Add start and exit bootstrap routines to this. Enable BootStrapper in App.xaml.cs (OnStartup and OnExit) (Listing 9a)
    • Bootstrapper holds a static instance to TodoService .In real world we would want to use proper dependency injection and use it to instantiate ITodoService whenever we require. Here we will use BootStrapper's static TodoService instance whenever we want.
    • Let us test the code if it is working. Move the dummy list filler to TodoService. Use TodoService to fill the list in HomeViewModel. (Listing 9c) (Listing 9d)
    • Test the TodoService is now working. Lets write our CRUD display and functionality now.


    • Milestone 3: Added Service and tested its binding.

      (Objective 4: Setup Crud Operations.)
  10. Now we need some helper classes to create our CRUD operations. What we will be doing now is pretty much standard MVVM CRUD operations.
    • -Let us first of all add our Common\BaseViewModel abstract class. This class implements INotifyPropertyChanged which is used by WPF/Xaml to dynamically bind property changes with view. This class also has a IsDirty flag which is marked true as soon as something is modified. (Listing 10a)
    • Add \Common\RelayCommand. The purpose of this class is as its name suggests - it will relay the command to appropriate Delegate defined in instance. There is a lot of information available online about this class. (Listing 10b)
    • One thing handy to WPF/Xaml are converters. Sometimes what Xaml wants are specific types (like Visibility, Color ) and we want to bind these to our properties of different kind (like a boolean flag IsVisible). Now to convert from one Type to other (the one required by Xaml) we implement Converters to convert back and provide Xaml with what it really wants. We will add some Converters here. We can notice tese Converters are converting from one type to other. (Listing 10c)
    • Add Common Styles in \Styles.xaml and add its reference to App.xaml (). Add commonly used converters & styles here in ResourceDictionary. (Listing 10d)
  11. The Next thing we want to do now is to update our TodoViewModel. This is the basic ViewModel class with which our MVVM application interacts.
    • As MVVM will require some presentation related properties and commands, and we don't want to add these presentation properties to our Service Entities, we will wrap our Service/Domain entities with ViewModel wrapper. Our TodoViewModel is basically a wrapper around Service.Entity.Todo. Our MVVM will interact with TodoViewModel which in turn will modify some of the properties of Enclosed Service.Entity.Todo. When we want to talk to our Service, we will simply remove the wrapper (TodoViewModel properties) and send over the enclosed Service.Entity.Todo to the service. -Let us add our TodoViewModel wrapper now. This Class inherits from BaseViewModel. TodoViewModel contains some MVVM properties as well. (Listing 11a)
  12. We add the CRUD operations now:
    • We will create our Home.xaml now with basic CRUD operations display. Notice that the buttons are not working as commands are not bound to real delegates yet. (Listing 12a)
    • Add the CRUD operations in HomeViewModel. (Listing 12b)
      • Add Default and custom ErrorTemplate(textBoxErrorTemplate) to Styles.xaml and use this template for validation of Textboxes.
      • Add a simple NumericValidationRule and update the Binding of target TextBoxes.
      • We also added simple Event To Command (Behavior) DoubleClick and hook it to ListView's items DoubleClick takes us to details page.
  13. Next up we will create the TodoDetails.xaml and Details page should take its DataContext from supplied TodoViewModel. Watch out for there is a catch here how we are collecting the TodoViewModel from Navigation and overriding it in the constructor of our "TodoDetails.xaml.cs". (Listing 13a)
    • There is some basic catch we need to notice here regarding Navigation. On load of the view we are overriding the DataContext with the supplied ViewModel instead of default one. Notice the changes in TodoDetails parametrized constructor TodoDetails.xaml.cs. (Listing 13b)
    • Notice the Back and Save buttons functionality in TodoViewModel bound to TodoDetails view.


    • Milestone 4: Added MVVM CRUD Operations.

      (Objective 5: Add Persistence Layer to save our data.)
  14. Add the Persistence layer. (Listing 14a) (Listing 14b) (Listing 14c)
    • We right click the MvvmCrudGv.Service project, "Manage Nuget Packages...." and installed "protobuf-net" package here.
    • We added the TodoPersistence class and associated ProtobufDB and helper classes. Also update our DataContract "Todo" with [ProtoContract],[Serializable] and its properties with [ProtoMember(<int>)] attributes.
    • Update the ITodoService to use TodoPersistence instead of simple in-memory list.

Milestone 5: Persistence Layer added.

So this completes our MVVM application.  Below are all the listings from code itself. It is annotated in code and little bit of explaination is added below. I hope the code itself is simple and self explainatory. We can move back and forth between the summary and code listing using links.

Code Listings:-

 

1a. So the first thing we do here is to create the wpf project in Visual studio (I used Microsoft Visual Studio Express 2013 for Windows Desktop which is available free online). There’s a number of quick starts online about wpf projects. What we did basically is:-

  • Open Visual Studio > Go to File > New > Project
  • From the popup which opens from left menue we choose Templates > Visual C# > Windows, then select WPF Application
  • Give the name “MvvmCrudGv” to the target application and chose the location for it, click OK.

(Back to Summary)

2a. Once we are done creating the folders below is how the structure of our application will look like:-

Image 4

(Back to Summary)

2b: Here is how our Todo class looks like (our actual DataContract which we will persist)

[DataContract(Namespace = "http://schemas.datacontract.org/2004/07/MvvmCrudGv.Service.Entity")]
public class Todo
{
	private Guid _Id;
	private string _Title;
	private string _Text;
	private DateTime _CreateDt;
	private DateTime _DueDt;
	private int _EstimatedPomodori;
	private int _CompletedPomodori;
	private string _AddedBy;

	[DataMember]
	public Guid Id
	{
		get { return _Id; }
		set {
			if (!_Id.Equals(value))
			{
				_Id = value;
			}
		}
	}

	[DataMember]        
	public string Title
	{
		get { return _Title; }
		set { _Title = value; }
	}


	[DataMember]
	public string Text
	{
		get { return _Text; }
		set { _Text = value; }
	}

	[DataMember]
	public DateTime CreateDt
	{
		get { return _CreateDt; }
		set { _CreateDt = value; }
	}

	[DataMember]
	public DateTime DueDt
	{
		get { return _DueDt; }
		set { _DueDt = value; }
	}

	[DataMember]
	public int EstimatedPomodori
	{
		get { return _EstimatedPomodori; }
		set { _EstimatedPomodori = value; }
	}

	[DataMember]
	public int CompletedPomodori
	{
		get { return _CompletedPomodori; }
		set { _CompletedPomodori = value; }
	}

	[DataMember]
	public string AddedBy
	{
		get { return _AddedBy; }
		set { _AddedBy = value; }
	}

	public Todo()
	{
		_Id = Guid.NewGuid();
		_CreateDt = DateTime.Now;
		_EstimatedPomodori = 1;
		_CompletedPomodori = 0;
		_DueDt = DateTime.Today.AddDays(1);
	}
}

(Back to Summary)

2c: Below is the dummy code we used in the view to bind the properties of our List Items to a ListView. Here's the ListView with ItemsSource binds directly to the TodoList. In items inside bind to the properties.

<ListView ItemsSource="{Binding}" Grid.Row="0" Grid.Column="0">
	<ListView.ItemTemplate>
		<DataTemplate>
			<StackPanel>
				<TextBlock Text="{Binding Title}" />
				<TextBlock Text="{Binding Text}" />
				<TextBlock Text="{Binding CreateDt}" />
			</StackPanel>
		</DataTemplate>
	</ListView.ItemTemplate>
</ListView>

(Back to Summary)

2d: This is our initial MainWindow class with our dummy TodoList.  We just added some items to TodoList. This list is bound to our view above. Once we add this code, we check if it works. We will keep checking back this display every time we make changes.  We are not writing Test Cases here (yeah I know that's bad practice. We should move it to our nearby objective. For now, our most of testing will be directly User testing it.)

public List<MvvmCrudGv.Service.Entity.Todo> TodoList { get; set; }

public MainWindow()
{
	TodoList = new List<MvvmCrudGv.Service.Entity.Todo>();
	TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" });
	TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" });
	this.DataContext = TodoList;
	InitializeComponent();
}

(Back to Summary)

4a. This is basic  MainWindowViewModel class. Soon we will move the above TodoList here.

C#
public class MainWindowViewModel(){}

(Back to Summary)

5a. Our ViewModelLocator. This is our commonplace where Views will bind to find out their ViewModels.

C#
class ViewModelLocator
    {
        public MainWindowViewModel MainWindowViewModel { get { return new MainWindowViewModel(); } }
        // public HomeViewModel HomeViewModel { get { return new HomeViewModel(); } }
        // public TodoViewModel TodoDetailsViewModel { get { return new TodoViewModel(); } }
    }

(Back to Summary)

5b.  In the class ViewModelLocator we exposed a Property named MainWindowViewModel - our MainWindow.xaml(view) will bind to this propertyh of ViewModelLocator. We are returning an instance of MainWindowViewModel class directly here. I real world app we would prefer dependency injection with appropriate configuration.

public MainWindowViewModel MainWindowViewModel { get { return new MainWindowViewModel(); } }

(Back to Summary)

5c.  Here is how we declared the ViewModelLocator instance in App.xaml and give it the key "Locator" so that our views can bind to this class.

<ResourceDictionary>
	<vm:ViewModelLocator x:Shared="False"  x:Key="Locator" xmlns:vm="clr-namespace:MvvmCrudGv.Common" />
	<ResourceDictionary.MergedDictionaries>
	</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

(Back to Summary)

5d. This is how we use the above instance of ViewModelLocator in our Views.  This line of code we will add to "MainWindow.xaml" at the very top.

C#
DataContext="{Binding MainWindowViewModel, Source={StaticResource Locator}}"

(Back to Summary)

5e. As our binding is now to the class "MainWindowViewModel" rather than to the TodoList, we will bind to the "TodoList" property inside our MainWindowViewModel class.  This how our ListView will be bound now in "MainWindow.xaml"

ListView ItemsSource="{Binding TodoList}"

(Back to Summary)

5f. Below is how we moved the code from MainWindow.xaml.cs to MainWindowViewModel.

public List<MvvmCrudGv.Service.Entity.Todo> TodoList { get; set; }

public MainWindowViewModel()
{
	TodoList = new List<MvvmCrudGv.Service.Entity.Todo>();
	TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" });
	TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" });
}

(Back to Summary)

6a. This is our EventAggregator. We can think of EventAggregator as a commonplace for all communication in our application.

Image 5

We can think of EventAggregator as a TV/Network cable connection operator.  There are a number of Publishers who are continuously publishing different Kinds of events(or programs) all the time. It is up to us,  which type of event(or progam) we want to Subscribe. EventAggregator keeps a log of all the Subscriptions and carries all the events altogether. And once the event is published(or a program is aired), if we have already subscribed to that event our corresponding handler will be invoked (when we subscribe we already tell the EventAggregator about which handler should be invoked.)

Moreover, Event publish and subscribe (or attach handler) in Microsoft basically has a catch.  This is much properly described by Sacha Barber, in his article and as he points the link to- Josh Smith the seasoned craftsman of Mvvm. Basically once we Subscribe to an event(by attaching a handler), due to our handler holding on super tight (Strong Reference) to the event, garbage collector is never able to free it up even if my handler no longer needs it.

So Microsoft has provided a class "WeakReference" to wrap our handler. This is what the "WeakActionRef" class at bottom of below code listing is doing.

Moreover, as we mentioned our EventAggregator class maintains a dictionary of Events (by Event "Type" mapped to "List of Handlers"(wrapped in WeakActionRef of course)).  Rest of the publish and subscribe code is just adding and removing from this dictionary as annotated below.

/// <summary>
/// Contract for EventAggregator
/// </summary>
public interface IEventAggregator
{
//Any class should be able toSubscribe to an event type
void Subscribe<T>(Action<T> handler);
//Any class should be able to Unsubscribe from an event type
void Unsubscribe<T>(Action<T> handler);
//Any class should be able to Publish an event type
void Publish<T>(T evt);
}

/// <summary>
/// This class maintains a dictionary of Events by their "Type" and 
/// a WeakReference to corresponding "event handlers" logged by Subscribers to that event.
/// Any class can publish an event by loggint it here first.
/// Any class can subscribe to events of particular type logging their subscription here.
/// </summary>
public sealed class EventAggregator : IEventAggregator
{
private Dictionary<Type, List<WeakActionRef>> _subscribers =
	new Dictionary<Type, List<WeakActionRef>>();

private object _lock = new object();

//Subscribe to an event type
public void Subscribe<T>(Action<T> handler)
{
	lock (_lock)
	{
		if (_subscribers.ContainsKey(typeof(T))) 
		{
			//Entry for this event type exists so we add our handler to dictionary
			var handlers = _subscribers[typeof(T)];
			handlers.Add(new WeakActionRef(handler));
		}
		else 
		{
			//Dictionary entry for this event type is empty so create new key and add handler to it
			var handlers = new List<WeakActionRef>();
			handlers.Add(new WeakActionRef(handler));
			_subscribers[typeof(T)] = handlers;
		}
	}
}

//Unsubscribe from an event type
public void Unsubscribe<T>(Action<T> handler)
{
	lock (_lock)
	{
		if (_subscribers.ContainsKey(typeof(T)))
		{
			var handlers = _subscribers[typeof(T)];

			//Find out the targetReference to be removed
			WeakActionRef targetReference = null;
			foreach (var reference in handlers)
			{
				var action = (Action<T>)reference.Target;
				if ((action.Target == handler.Target) && action.Method.Equals(handler.Method))
				{
					targetReference = reference;
					break;
				}
			}
			//Remove the targetReference
			handlers.Remove(targetReference);

			//If there are no more handlers/subscribers for this event type
			if (handlers.Count == 0)
			{
				_subscribers.Remove(typeof(T));
			}
		}
	}
}

//Publish an event type
public void Publish<T>(T evt)
{
	lock (_lock)
	{
		if (_subscribers.ContainsKey(typeof(T)))
		{
			var handlers = _subscribers[typeof(T)];
			foreach (var handler in handlers)
			{
				if (handler.IsAlive)
				{
					//If the handler is still alive Invoke it
					((Action<T>)handler.Target).Invoke(evt);
				}
				else
				{
					//Otherwise just remove the handler from dictionary
					handlers.Remove(handler);
				}
			}

			//If the number of handlers is zero, remove empty Type entry from Dictionary
			if (handlers.Count == 0)
			{
				_subscribers.Remove(typeof(T));
			}
		}
	}
}

}

/// <summary>
/// A wrapper to handler. Wraps it in WeakReference
/// so that we can check the WeakReference every time
/// and remove it if it isn't alive anymore
/// </summary>
public sealed class WeakActionRef
{
private WeakReference WeakReference { get; set; }

public Delegate Target { get; private set; }

public bool IsAlive
{
	get { return WeakReference.IsAlive; }
}

//At creation maintain a weakreference to the target
public WeakActionRef(Delegate action)
{
	Target = action;
	WeakReference = new WeakReference(action.Target);
}
}

(Back to Summary)


6b.  Here is one of our Event type, or MessageType which we publish.  This is called NavMessage because as we will notice here, we are passing some parameters related to Navigation.  Basically our MainWindow will subscribe to this NavMessage and do navigation for us. In more elaborate framework, we might have proper Contracts and our Windows and Pages usually inherit from these contracts so that Navigation service can perform Navigation actions among them.  Here we try to keep it simple - still our Navigation system is capable of navigating us using NavMessage. (So lets be careful before publishing NavMessage in the system as system will automatically seek to navigate once it detects this message. Our MainWindow subscribes to this message and navigates the MainFrame accordingly)

/// <summary>
/// Used to Publish/Subscribe a Navigation event
/// </summary>
public class NavMessage
    {
        private string Notification;

        public string PageName
        {
            get { return this.Notification; }
        }


        public Dictionary<string, string> QueryStringParams { get; private set; }
        public object NavigationStateParams { get; private set; }
        public object ViewObject { get; private set; }

        public NavMessage(string pageName)
        {
            this.Notification = pageName;
        }

        public NavMessage(string pageName, Dictionary<string, string> queryStringParams)
            : this(pageName)
        {
            QueryStringParams = queryStringParams;
        }

        //Pass the instance of the View class and the ViewModel
        public NavMessage(object viewObject, object navigationStateParams)
            : this(viewObject.GetType().Name)
        {
            ViewObject = viewObject;
            NavigationStateParams=navigationStateParams;
        }
    }

    public class ObjMessage
    {
        public string Notification { get; private set; }
        public object PayLoad { get; private set; }

        public ObjMessage(string pageName, object payLoad)
        {
            Notification = pageName;
            PayLoad = payLoad;
        }
    }

(Back to Summary)

7c. We move our dummy ListView from MainWindow.xaml to Home.xaml. Nothing has changed here, we just moved the ListView.

<Grid>
	<ListView ItemsSource="{Binding TodoList}" Grid.Row="0" Grid.Column="0">
		<ListView.ItemTemplate>
			<DataTemplate>
				<StackPanel>
					<TextBlock Text="{Binding Title}" />
					<TextBlock Text="{Binding Text}" />
					<TextBlock Text="{Binding CreateDt}" />
				</StackPanel>
			</DataTemplate>
		</ListView.ItemTemplate>
	</ListView>
</Grid>

(Back to Summary)


7d. And here is how our HomeViewModel will look like. As we see here, nothing new, just moved the MainWindowViewModel's  dummy list to the HomeViewModel.

public class HomeViewModel
{
	public List<MvvmCrudGv.Service.Entity.Todo> TodoList { get; set; }

	public HomeViewModel()
	{
		TodoList = new List<MvvmCrudGv.Service.Entity.Todo>();
		TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" });
		TodoList.Add(new MvvmCrudGv.Service.Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" });
	}
}

(Back to Summary)


7e. And here is how our MainWindow.xaml now looks like. As we notice here, we have a Simple Grid with two rows. The first row has a menu, the second row holds a Frame. Simple, nothing fancy.

<Window x:Class="MvvmCrudGv.MainWindow"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	Title="MVVM WCF CRUD Operations" Height="350" Width="525"
	DataContext="{Binding MainWindowViewModel, Source={StaticResource Locator}}">
<Grid>
	<Grid.RowDefinitions>
		<RowDefinition Height="26"></RowDefinition>
		<RowDefinition Height="*"></RowDefinition>
	</Grid.RowDefinitions>
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="*"></ColumnDefinition>
	</Grid.ColumnDefinitions>
	<Grid Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Name="grdMenuNav" Height="26">
		<Border BorderBrush="Gray" BorderThickness="1">
			<Menu IsMainMenu="True" Grid.Row="0" Grid.Column="0" Margin="0" Padding="5,0" Height="22" Background="White">
				<MenuItem Header="_File" Height="22">
					<MenuItem Header="_Exit" Command="{Binding ExitCmd}" />
				</MenuItem>
			</Menu>
		</Border>
	</Grid>
	<DockPanel Grid.Row="1" Width="Auto">
		<Frame x:Name="_MainFrame" NavigationUIVisibility="Hidden" />
	</DockPanel>
</Grid>
</Window>

(Back to Summary)


7f. These are the properties added to ViewModelLocator which will ezpose the HomeViewModel and TodoViewModel. Our respective views (Home.xaml and TodoDetails.xaml) will bind to these.

public HomeViewModel HomeViewModel { get { return new HomeViewModel(); } }
public TodoViewModel TodoDetailsViewModel { get { return new TodoViewModel(); } }

(Back to Summary)


7g. Here are the lines which we will add to Home.xaml and TodoDetails.xaml to  bind to our above properties.

DataContext="{Binding HomeViewModel, Source={StaticResource Locator}}"
DataContext="{Binding TodoDetailsViewModel, Source={StaticResource Locator}}"

(Back to Summary)

7h. We added a static EventAggregator to our Application.  As we notice here, we instantiated it at the Application startup. As it is static, it wouldn't be disposed when its's not in use. We will moslty use it for all the communications so we made it static and put it at the App level.

public partial class App : Application
{
	public static IEventAggregator eventAggregator { get; private set; }

	protected override void OnStartup(StartupEventArgs e)
	{
		//Common.BootStrapper.Instance.Bootstrap(this,e);
		eventAggregator = new EventAggregator();
		base.OnStartup(e);
	}

	

	protected override void OnExit(ExitEventArgs e)
	{
		//Common.BootStrapper.Instance.ShutDown(this, e);
		base.OnExit(e);
	}
}

(Back to Summary)

7i. Notice here how our MainWindow subscribes to NavMessage. As soon as our MainWindow recevies the "NavMessage" notification over EventAggregator channel, it Navigates the "MainFrame" to the desired destination mentioned in NavMessage. Once the MainFrame is navigated (Container_LoadCompleted), the MainWindow publishes ObjMessage with the intended payload. The target ViewModel can then subscribe to ObjMessage to receive the payload thus re-directed by MainWindow.

C#
/// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        IEventAggregator _eventAggregator;

        public MainWindow()
        {
            _eventAggregator = App.eventAggregator;
            InitializeComponent();
            _MainFrame.NavigationService.LoadCompleted += new LoadCompletedEventHandler(container_LoadCompleted);
            _MainFrame.NavigationService.Navigate(new Home());
            _eventAggregator.Subscribe<NavMessage>(NavigateToPage);
        }

        private void NavigateToPage(NavMessage message)
        {
            object viewObject = message.ViewObject;
            object navigationState = message.NavigationStateParams;

            if ((viewObject!=null)&&(navigationState!=null))
            {
                _MainFrame.NavigationService.Navigate(viewObject, navigationState);

                return;
            }
            else if (viewObject!=null)
            {
                _MainFrame.NavigationService.Navigate(viewObject);
                return;
            }

            //Silverlight
            string queryStringParams = message.QueryStringParams == null ? "" : GetQueryString(message);
            string uri = string.Format("/Views/{0}.xaml{1}", message.PageName, queryStringParams);
            _MainFrame.NavigationService.Navigate(new Uri(uri, UriKind.Relative));
        }

        void container_LoadCompleted(object sender, NavigationEventArgs e)
        {
            if (e.ExtraData != null)
            _eventAggregator.Publish<ObjMessage>(new ObjMessage(e.Content.GetType().Name, e.ExtraData));

        }

        private string GetQueryString(NavMessage message)
        {
            string qstr = null;
            if (message.QueryStringParams != null)
            {
                qstr = string.Concat(message.QueryStringParams.Select(x => x.Key + "=" + x.Value).ToList<string>().ToArray());
                qstr = "?" + qstr;
            }
            return (qstr);
        }
    }

(Back to Summary)


8a. Below is our Service Layer.  We have our "ServiceContract" or the protocol with which we will be able to to talk to our service class "ITodoService" where we have basic CRUD Operations. Our implementation below "TodoService" implements above contract and currently we are just storing our Todo's in an in-memory List as below. We will soon replace this in-memory list with actual persistence. But for now this should get our App running.

[ServiceContract]
public interface ITodoService
{
	[OperationContract]
	Guid Add(Todo todo);
	[OperationContract]
	void Delete(Guid id);
	[OperationContract]
	List<Todo> List();
	[OperationContract]
	bool Update(Todo todo);
	[OperationContract]
	Todo Get(Guid id);
}

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, UseSynchronizationContext = false)]
public class TodoService:ITodoService
{
	List<Todo> _lstDb = new List<Todo>();

	public Guid Add(Todo todo)
	{
		_lstDb.Add(todo);
		return (todo.Id);
	}

	public Todo Get(Guid id)
	{
		return (_lstDb.Where(x => x.Id.ToString() == id.ToString()).FirstOrDefault());
	}

	public void Delete(Guid id)
	{
		_lstDb.Remove(_lstDb.Where(x => x.Id.ToString() == id.ToString()).FirstOrDefault());
	}

	public List<Todo> List()
	{
		return (_lstDb);
	}

	public bool Update(Todo todo)
	{
		var itm = _lstDb.Where(x => x.Id == todo.Id).FirstOrDefault();
		if (itm == null)
		{
			return false;
		}
		else
		{
			_lstDb[_lstDb.IndexOf(itm)] = todo;
			return (true);
		}
	}
}

(Back to Summary)

9a. This is our BootStrapper class. As mentioned above, this is a simple singleton. Why Singleton? We want a commonplace instance for our Startup and Shutdown routines.  As we notice here, we are instantiating another static Instance of our TodoService here.  In real world, we would rather use dependency injection and DI will instrantiate it wherever we want. We wouldn't put this Service class in BootStrapper, but as we need an instance anyways, we are creating a re-usable static instance here as below.

class BootStrapper
{
	private static BootStrapper _instance;
	private static ITodoService _todoService;

	public ITodoService TodoService { get { return (_todoService); } }

	private BootStrapper()
	{
		_todoService = new TodoService();
	}

	public static BootStrapper Instance { get {
		if (_instance==null)
		{
			_instance = new BootStrapper();
		}
		return (_instance);
	} 
	}

	public void Bootstrap(App app, System.Windows.StartupEventArgs e)
	{
		//Do bootstap here
	}

	public void ShutDown(App app, System.Windows.ExitEventArgs e)
	{
		//Do shutdown cleanup here
	}
}

(Back to Summary)


9c. This Dummy filler code we can move to the TodoService class and check that it is returning us a list.

if ((_lstDb==null)||(_lstDb.Count==0))
{
	_lstDb.Add(new Entity.Todo() { Title = "dummy1 title", Text = "Dummy1 Text" });
	_lstDb.Add(new Entity.Todo() { Title = "dummy2 title", Text = "Dummy2 Text" });
}

(Back to Summary)


9d. In our HomeViewModel we will bind to the list returned by TodoService.

public HomeViewModel()
{
	TodoList = MvvmCrudGv.Common.BootStrapper.Instance.TodoService.List();
}

(Back to Summary)

10a. Our BaseViewModel class. As mentioned above, it implements INotifyPropertyChanged and raises OnPropertyChanged if any property is modified. This class also holds a IsDirty boolean poperty which we would want to mark true as soon as any of properties is modified - so that we know when an object has been modified.

 

public class BaseViewModel : INotifyPropertyChanged
{
	private bool _IsDirty;

	public virtual bool IsDirty
	{
		get { return _IsDirty; }
		set
		{
			if (_IsDirty != value)
			{
				_IsDirty = value;
				OnPropertyChanged("IsDirty");
			}
		}
	}

	public event PropertyChangedEventHandler PropertyChanged;

	/// <summary>
	/// When property is changed call this method to fire the PropertyChanged Event
	/// </summary>
	/// <param name="propertyName"></param>
	public void OnPropertyChanged(string propertyName)
	{
		//Fire the PropertyChanged event in case somebody subscribed to it
		if (PropertyChanged != null)
		{
			PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			if (!propertyName.Equals("IsDirty"))
			{ IsDirty = true; }
		}
	}

	public void OnPropertyChanged<T>(Expression<Func<T>> property)
	{
		if (PropertyChanged != null)
		{
			var memberExpression = property.Body as MemberExpression;
			PropertyChanged(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
			if (!memberExpression.Member.Name.Equals("IsDirty"))
			{ IsDirty = true; }
		}
	}
}

(Back to Summary)

10b. Here is RelayCommand class. This is a very common practice, the purpose of this class is to Relay the Command to appropriate Action which we will instantiate it with.

 

/// <summary>
/// A command whose sole purpose is to 
/// relay its functionality to other
/// objects by invoking delegates. The
/// default return value for the CanExecute
/// method is 'true'.
/// </summary>
public class RelayCommand : ICommand
{
	#region Fields

	readonly Action<object> _execute;
	readonly Predicate<object> _canExecute;

	#endregion // Fields

	#region Constructors

	/// <summary>
	/// Creates a new command that can always execute.
	/// </summary>
	/// <param name="execute">The execution logic.</param>
	public RelayCommand(Action<object> execute)
		: this(execute, null)
	{
	}

	/// <summary>
	/// Creates a new command.
	/// </summary>
	/// <param name="execute">The execution logic.</param>
	/// <param name="canExecute">The execution status logic.</param>
	public RelayCommand(Action<object> execute, Predicate<object> canExecute)
	{
		if (execute == null)
			throw new ArgumentNullException("execute");

		_execute = execute;
		_canExecute = canExecute;
	}

	#endregion // Constructors

	#region ICommand Members

	[DebuggerStepThrough]
	public bool CanExecute(object parameter)
	{
		return _canExecute == null ? true : _canExecute(parameter);
	}

	public event EventHandler CanExecuteChanged
	{
		add { CommandManager.RequerySuggested += value; }
		remove { CommandManager.RequerySuggested -= value; }
	}

	public void Execute(object parameter)
	{
		_execute(parameter);
	}

	#endregion // ICommand Members
}

(Back to Summary)

 

10c. Below are some of Utility converters we normally need in WPF.  Basically convert back from Boolean to Visibility, Reverse of Boolean to Visibilty, We also have an Enum EditMode so we also wanted to convert from our Enum value to Visibilty.  Then we have a converter to handle long strings in our list. Because long strings might not be very pleasant in our list and might make it go non-uniform so we created a converter to truncate & beautify the string for our ListView.

 

public enum EditMode { Create, Update }

public class InverseBooleanConverter : IValueConverter
{
	#region IValueConverter Members

	public object Convert(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		if (targetType != typeof(Visibility))
			throw new InvalidOperationException("The target must be a boolean");

		return (!((bool)value));
	}

	public object ConvertBack(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		throw new NotSupportedException();
	}

	#endregion
}

public class BooleanToVisibilityConverter : IValueConverter
{
	#region IValueConverter Members

	public object Convert(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		if (targetType != typeof(Visibility))
			throw new InvalidOperationException("The target must be a boolean");

		if ((bool)value)
		{
			return Visibility.Visible;
		}
		return Visibility.Collapsed;
	}

	public object ConvertBack(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		throw new NotSupportedException();
	}

	#endregion
}

public class InverseBooleanToVisibilityConverter : IValueConverter
{
	#region IValueConverter Members

	public object Convert(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		if (targetType != typeof(Visibility))
			throw new InvalidOperationException("The target must be a boolean");

		if (!(bool)value)
		{
			return Visibility.Visible;
		}
		return Visibility.Collapsed;
	}

	public object ConvertBack(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		throw new NotSupportedException();
	}

	#endregion
}


public class EditModeToVisibilityConverter : IValueConverter
{
	#region IValueConverter Members

	public object Convert(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		if (targetType != typeof(Visibility))
			throw new InvalidOperationException("The target must be a boolean");

		EditMode mode= (EditMode)Enum.Parse(typeof(EditMode),parameter.ToString());
		if(((EditMode)value).Equals(mode))
		{
			return Visibility.Visible;
		}
		return Visibility.Collapsed;
	}

	public object ConvertBack(object value, Type targetType, object parameter,
		System.Globalization.CultureInfo culture)
	{
		throw new NotSupportedException();
	}

	#endregion
}

public class TextTruncateConverter : IValueConverter
{
	public object Convert(object value, Type targetType,
		object parameter, System.Globalization.CultureInfo culture)
	{
		if (value == null)
			return string.Empty;
		if (parameter == null)
			return value;
		int _MaxLength;
		if (!int.TryParse(parameter.ToString(), out _MaxLength))
			return value;
		var _String = value.ToString().Replace("\r\n", "... ").Replace("\n", "... ").Replace("\r", "... ");
		if (_String.Length > _MaxLength)
			_String = _String.Substring(0, _MaxLength) + "...";
		return _String;
	}

	public object ConvertBack(object value, Type targetType,
		object parameter, System.Globalization.CultureInfo culture)
	{
		throw new NotImplementedException();
	}
}

(Back to Summary)

 

10d. This is our ResourceDictionary. Here we have instantiated most of our converters so that we can use them wherever we want. We also added some re-usable styles here.  We will notice at the bottom of our ResourceDictionary is an ErrorTemplate which we will use for display of our Validation messages.

 

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
				xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
				xmlns:c="clr-namespace:MvvmCrudGv.Common">
<c:BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter" />
<c:InverseBooleanToVisibilityConverter x:Key="inverseBooleanToVisibilityConverter" />
<c:EditModeToVisibilityConverter x:Key="editModeToVisibilityConverter" />
<c:TextTruncateConverter x:Key="textTruncateConverter" />

<Style TargetType="{x:Type Border}" x:Key="grayBgBorder">
	<!-- All rows -->
	<Setter Property="BorderBrush" Value="Gray" />
	<Setter Property="BorderThickness" Value="1" />
	<Setter Property="CornerRadius" Value="0" />
	<Setter Property="Background" Value="LightGray" />
</Style>
<Style TargetType="{x:Type Border}" x:Key="grayBorder">
	<!-- All rows -->
	<Setter Property="BorderBrush" Value="Gray" />
	<Setter Property="BorderThickness" Value="1" />
	<Setter Property="CornerRadius" Value="0" />
</Style>
<ControlTemplate x:Key="textBoxErrorTemplate">
	<DockPanel LastChildFill="True">
		<TextBlock DockPanel.Dock="Bottom" 
	Foreground="Orange" 
	FontSize="12pt">**</TextBlock>
		<Border BorderBrush="Red" BorderThickness="1">
			<AdornedElementPlaceholder />
		</Border>
	</DockPanel>
</ControlTemplate>
<!-- this is default error template for all textboxes unless we choose custom one-->
<Style TargetType="{x:Type TextBox}">
	<Setter Property="Validation.ErrorTemplate">
		<Setter.Value>
			<ControlTemplate>
				<DockPanel LastChildFill="True">
					<TextBlock DockPanel.Dock="Bottom" 
					Foreground="Orange"
					FontSize="12pt"
					Text="{Binding ElementName=MyErrorAdorner,Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" Visibility="{Binding XPath=AdornedElement.(Validation.HasErrors), Converter={StaticResource ResourceKey=booleanToVisibilityConverter}}">
					</TextBlock>
					<Border BorderBrush="Red" BorderThickness="1">
						<AdornedElementPlaceholder Name="MyErrorAdorner" />
					</Border>
				</DockPanel>
			</ControlTemplate>
		</Setter.Value>
	</Setter>
	<Style.Triggers>
		<Trigger Property="Validation.HasError" Value="true">
			<Setter Property="ToolTip"
			Value="{Binding RelativeSource={RelativeSource Self}, 
				   Path=(Validation.Errors)[0].ErrorContent}"/>
		</Trigger>
	</Style.Triggers>
</Style>

</ResourceDictionary>

(Back to Summary)

 

11a. As mentioned above, this is our TodoViewModel which is just a wrapper around base Entity "Todo".  It exposes all the properties of base "Todo" entity wrapping them with some MVVM specific annotations. It also contains some commands which our MVVM needs to work on the ViewModel.

public class TodoViewModel : BaseViewModel
{

	public Todo _todo { get; private set; }


	public TodoViewModel()
		: this(new Todo())
	{

	}

	public TodoViewModel(Todo todo)
	{
		_todo = todo;
	}


	#region Properties
	public override bool IsDirty
	{
		get { return base.IsDirty; }
		set
		{
			if (base.IsDirty != value)
			{
				base.IsDirty = value;
				OnPropertyChanged("IsDirty");
			}
		}
	}

	public Guid Id
	{
		get { return _todo.Id; }
		set
		{
			_todo.Id = value;
			OnPropertyChanged("Id");
		}
	}

	public string Title
	{
		get { return _todo.Title; }
		set
		{
			_todo.Title = value;
			OnPropertyChanged("Title");
		}
	}

	public string Text
	{
		get { return _todo.Text; }
		set
		{
			_todo.Text = value;
			OnPropertyChanged("Text");
		}
	}


	public DateTime CreateDt
	{
		get { return _todo.CreateDt; }
		//set { _todo.CreateDt = value;
		//OnPropertyChanged("CreateDt");
		//}
	}

	public DateTime DueDt
	{
		get { return _todo.DueDt; }
		set
		{
			_todo.DueDt = value;
			OnPropertyChanged("DueDt");
		}
	}

	public int EstimatedPomodori
	{
		get { return _todo.EstimatedPomodori; }
		set
		{
			_todo.EstimatedPomodori = value;
			OnPropertyChanged("EstimatedPomodori");
		}
	}

	public int CompletedPomodori
	{
		get { return _todo.CompletedPomodori; }
		set
		{
			_todo.CompletedPomodori = value;
			OnPropertyChanged("CompletedPomodori");
		}
	}

	public string AddedBy
	{
		get { return _todo.AddedBy; }
		set
		{
			_todo.AddedBy = value;
			OnPropertyChanged("AddedBy");
		}
	}
	#endregion

}

(Back to Summary)

12a. Here is our Home.xaml.  This is our CRUD ListView.  We will notice here, there is a Grid Layout with three rows. The first row contains the Header of our CRUD List. The second row contains the actual List. The Third row contains a Grid again which houses our Editing Textboxes and a horizontal StackPanel of buttons.  We are toggling the Visibiltiy of our buttons using our Converters. (We have commented out some of code here.

<Page x:Class="MvvmCrudGv.Views.Home"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
	Title="Home" DataContext="{Binding HomeViewModel, Source={StaticResource Locator}}">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="24"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="50"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid Grid.Row="0" Grid.Column="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="50"></ColumnDefinition>
                <ColumnDefinition Width="50"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <Border Grid.Row="0" Grid.Column="0" Style="{StaticResource grayBgBorder}"></Border>
            <Border Grid.Row="0" Grid.Column="1" Style="{StaticResource grayBgBorder}"></Border>
            <Border Grid.Row="0" Grid.Column="2" Style="{StaticResource grayBgBorder}"></Border>
            <Border Grid.Row="0" Grid.Column="3" Style="{StaticResource grayBgBorder}"></Border>
            <TextBlock  Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center">
                <Bold>
                    Title
                </Bold>
            </TextBlock>
            <TextBlock  Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center">
                <Bold>
                    Notes
                </Bold>
            </TextBlock>
            <TextBlock  Grid.Row="0" Grid.Column="2" HorizontalAlignment="Center">
                <Bold>
                    Estim.
                </Bold>
            </TextBlock>
            <TextBlock  Grid.Row="0" Grid.Column="3" HorizontalAlignment="Center">
                <Bold>
                    Cmplt.
                </Bold>
            </TextBlock>
        </Grid>
        <ListView ItemsSource="{Binding TodoList}" Grid.Row="1" Grid.Column="0" SelectedItem="{Binding SelectedTodo}" SelectedIndex="{Binding TodoListSelectedIndex}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch"
                ><!--:EventToCommand.Command="{Binding GoTodoDetailsCmd}" b:EventToCommand.CommandParameter="{Binding SelectedTodo}"  xmlns:b="clr-namespace:MvvmCrudGv.Common.Behaviors">-->
            <ListView.ItemTemplate>
                <DataTemplate>
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="*"></RowDefinition>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"></ColumnDefinition>
                            <ColumnDefinition Width="*"></ColumnDefinition>
                            <ColumnDefinition Width="50"></ColumnDefinition>
                            <ColumnDefinition Width="50"></ColumnDefinition>
                        </Grid.ColumnDefinitions>
                        <Border Grid.Row="0" Grid.ColumnSpan="4" Visibility="{Binding IsDirty,Converter={StaticResource booleanToVisibilityConverter}}" BorderBrush="Red" BorderThickness="1"/>
                        <TextBlock Text="{Binding Title}" Grid.Row="0" Grid.Column="0" HorizontalAlignment="Center" />
                        <TextBlock Text="{Binding Text, Converter={StaticResource textTruncateConverter},ConverterParameter=32}" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Center" />
                        <TextBlock Text="{Binding EstimatedPomodori}" Grid.Row="0" Grid.Column="2" HorizontalAlignment="Center"  />
                        <TextBlock Text="{Binding CompletedPomodori}" Grid.Row="0" Grid.Column="3" HorizontalAlignment="Center"  />
                    </Grid>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <Grid Grid.Row="2" Grid.Column="0">
            <Grid.RowDefinitions>
                <RowDefinition Height="24"></RowDefinition>
                <RowDefinition Height="24"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="50"></ColumnDefinition>
                <ColumnDefinition Width="50"></ColumnDefinition>
            </Grid.ColumnDefinitions>
            <TextBox Text="{Binding SelectedTodo.Title}" Grid.Row="0" Grid.Column="0" />
            <TextBox Text="{Binding SelectedTodo.Text}" Grid.Row="0" Grid.Column="1" />
            <TextBox Grid.Row="0" Grid.Column="2">
                <Binding Path="SelectedTodo.EstimatedPomodori">
                    <Binding.ValidationRules>
                        <!--<c:NumericValidator></c:NumericValidator>-->
                    </Binding.ValidationRules>
                </Binding>
            </TextBox>
            <TextBox Grid.Row="0" Grid.Column="3" >
                <Binding Path="SelectedTodo.CompletedPomodori">
                    <Binding.ValidationRules>
                        <!--<c:NumericValidator></c:NumericValidator>-->
                    </Binding.ValidationRules>
                </Binding>
            </TextBox>
            <StackPanel Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="4" Orientation="Horizontal" HorizontalAlignment="Right">
                <Button Command="{Binding NewTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="New"></Button>
                <Button  Command="{Binding AddTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Create}" Content="Add"></Button>
                <Button  Command="{Binding UpdateTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="Save"></Button>
                <Button  Command="{Binding GoTodoDetailsCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="Details"></Button>
                <Button  Command="{Binding DeleteTodoCmd}" Visibility="{Binding TodoListEditMode, Converter={StaticResource editModeToVisibilityConverter}, ConverterParameter=Update}" Content="Delete"></Button>
            </StackPanel>
        </Grid>
    </Grid>
</Page>

(Back to Summary)

12b. And here is how our HomeViewModel looks like.   As we will notice here, it contains some basic CRUD Commands.  Then we have  our ObservableCollection<TodoViewModel" TodoList which we bind to our ListView display.  Then we have a "SelectedTodo" property. As soon as we select an Item from the ListView, we mark tis reference to "SelectedTodo". Then we can perform our Editing operations on SelectedTodo and corresponding ListView Item will reflect the changes.  If we click "New", SelectedTodo is instantiated with a New empty TodoViewModel which we can Edit and "Save" it.

public class HomeViewModel : BaseViewModel
{

	public ICommand AddTodoCmd { get; private set; }
	public ICommand ListTodosCmd { get; private set; }
	public ICommand UpdateTodoCmd { get; private set; }
	public ICommand LoadTodoCmd { get; private set; }
	public ICommand DeleteTodoCmd { get; private set; }
	public ICommand NewTodoCmd { get; private set; }
	public ICommand GoTodoDetailsCmd { get; private set; }

	public ObservableCollection<TodoViewModel> TodoList { get; set; }
	private TodoViewModel _SelectedTodo;
	private int _TodoListSelectedIndex;
	private EditMode _TodoListEditMode;
	private IEventAggregator _eventAggregator;

	public EditMode TodoListEditMode
	{
		get { return _TodoListEditMode; }
		set
		{
			if (_TodoListEditMode != value)
			{
				_TodoListEditMode = value;
				OnPropertyChanged("TodoListEditMode");
			}
		}
	}

	public int TodoListSelectedIndex
	{
		get { return _TodoListSelectedIndex; }
		set
		{
			if (_TodoListSelectedIndex != value)
			{
				_TodoListSelectedIndex = value;
				if (_TodoListSelectedIndex == -1)
				{
					TodoListEditMode = EditMode.Create;
				}
				else
				{
					TodoListEditMode = EditMode.Update;
				}
				OnPropertyChanged("TodoListSelectedIndex");
			}
		}
	}

	public TodoViewModel SelectedTodo
	{
		get { return _SelectedTodo; }
		set
		{
			if ((null != value) && (_SelectedTodo != value))
			{
				_SelectedTodo = value;
				OnPropertyChanged("SelectedTodo");
			}
		}
	}

	MvvmCrudGv.Service.ITodoService _todoServiceClient;


	public HomeViewModel()
	{
		_todoServiceClient = BootStrapper.Instance.TodoService;
		_eventAggregator = App.eventAggregator;
		TodoList = new ObservableCollection<TodoViewModel>();


		loadTodoList();
		_TodoListSelectedIndex = -1;
		_SelectedTodo = new TodoViewModel();



		UpdateTodoCmd = new RelayCommand(ExecUpdateTodo, CanUpdateTodo);
		DeleteTodoCmd = new RelayCommand(ExecDeleteTodo, CanDeleteTodo);
		LoadTodoCmd = new RelayCommand(ExecLoadTodo, CanLoadTodo);
		ListTodosCmd = new RelayCommand(ExecListTodos, CanListTodos);
		AddTodoCmd = new RelayCommand(ExecAddTodo, CanAddTodo);
		NewTodoCmd = new RelayCommand(ExecNewTodo, CanNewTodo);
		GoTodoDetailsCmd = new RelayCommand(ExecGoTodoDetails, CanGoTodoDetails);
	}

	private void ExecNewTodo(object obj)
	{
		SelectedTodo = new TodoViewModel();
		TodoListSelectedIndex = -1;
	}

	[DebuggerStepThrough]
	private bool CanNewTodo(object obj)
	{
		return (true);
	}

	private void loadTodoList()
	{
		//Dummy
		if (!(_todoServiceClient.List().Count > 0))
		{
			var tid = _todoServiceClient.Add(new Service.Entity.Todo() { AddedBy = "Amit", Title = "First todo", Text = "this is first todo" });
		}

		var lstTodos = _todoServiceClient.List();
		if ((lstTodos != null) && (lstTodos.Count > 0))
		{
			foreach (var item in lstTodos)
			{
				TodoList.Add(new TodoViewModel(item));
			}
		}
	}


	//This goes in Initialization/constructor
	private void ExecDeleteTodo(object obj)
	{
		System.Windows.MessageBoxResult confirmRunResult = System.Windows.MessageBox.Show("Are you sure you want to delete this todo?", "Delete Item?", System.Windows.MessageBoxButton.OKCancel);
		if (confirmRunResult == System.Windows.MessageBoxResult.Cancel)
		{
			return;
		}
		_todoServiceClient.Delete(SelectedTodo.Id);
		TodoList.Remove(SelectedTodo);
		resetSelectedTodo();
	}

	private bool CanDeleteTodo(object obj)
	{
		return (true);
	}
	//This goes in Initialization/constructor
	private void ExecLoadTodo(object obj)
	{

	}

	private bool CanLoadTodo(object obj)
	{
		return (true);
	}
	//This goes in Initialization/constructor
	private void ExecUpdateTodo(object obj)
	{
		bool isok = _todoServiceClient.Update(SelectedTodo._todo);
		SelectedTodo.IsDirty = !isok;
	}

	private bool CanUpdateTodo(object obj)
	{
		return (true);
	}
	//This goes in Initialization/constructor
	private void ExecListTodos(object obj)
	{

	}

	private bool CanListTodos(object obj)
	{
		return (true);
	}
	//This goes in Initialization/constructor
	private void ExecAddTodo(object obj)
	{
		Guid addedid = _todoServiceClient.Add(SelectedTodo._todo);
		SelectedTodo.Id = addedid;
		SelectedTodo.IsDirty = false;
		TodoList.Add(SelectedTodo);
		resetSelectedTodo();
	}

	private void resetSelectedTodo()
	{
		SelectedTodo = new TodoViewModel();
		TodoListSelectedIndex = -1;
	}

	private bool CanAddTodo(object obj)
	{
		return (true);
	}

	private void ExecGoTodoDetails(object obj)
	{
		_eventAggregator.Publish<NavMessage>(new NavMessage(new MvvmCrudGv.Views.TodoDetails(), SelectedTodo));
	}

	private bool CanGoTodoDetails(object obj)
	{
		return (true);
	}
}

(Back to Summary)

13a.  Here is our TodoDetails page:-

XML
<Page x:Class="MvvmCrudGv.Views.TodoDetails"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300"
	Title="TodoDetails"
      xmlns:c="clr-namespace:MvvmCrudGv.Common"
      DataContext="{Binding TodoDetailsViewModel, Source={StaticResource Locator}}">

    <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="24"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="24"></RowDefinition>
            <RowDefinition Height="24"></RowDefinition>
            <RowDefinition Height="24"></RowDefinition>
        </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Border Grid.Row="0" Grid.RowSpan="5" Grid.ColumnSpan="2" Visibility="{Binding IsDirty,Converter={StaticResource booleanToVisibilityConverter}}" BorderBrush="Red" BorderThickness="1"/>
        <Label Grid.Row="0" Grid.Column="0" >Title</Label>
        <Label Grid.Row="1" Grid.Column="0" >Notes</Label>
        <Label Grid.Row="2" Grid.Column="0" >Estimated Hrs</Label>
        <Label Grid.Row="3" Grid.Column="0" >Completed Hrs</Label>
        <TextBox Text="{Binding CurrentTodo.Title}" Grid.Row="0" Grid.Column="1" />
        <TextBox Text="{Binding CurrentTodo.Text}" Grid.Row="1" Grid.Column="1" VerticalContentAlignment="Stretch" Height="Auto" AcceptsReturn="True" />
            <TextBox Grid.Row="2" Grid.Column="1">
            <Binding Path="CurrentTodo.EstimatedPomodori">
                    <Binding.ValidationRules>
                        <c:NumericValidator></c:NumericValidator>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox>
            <TextBox Grid.Row="3" Grid.Column="2" >
            <Binding Path="CurrentTodo.CompletedPomodori">
                    <Binding.ValidationRules>
                        <c:NumericValidator></c:NumericValidator>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox>
        <StackPanel Grid.Row="4" Grid.ColumnSpan="2" HorizontalAlignment="Right" Orientation="Horizontal">
            <Button Command="{Binding GoBackCmd}" Content="Back" Width="50"></Button>
            <Button Command="{Binding SaveTodoCmd}" Content="Save" Width="50"></Button>
        </StackPanel>
    </Grid>
</Page>

(Back to Summary)

13b. Please observe over here how our TodoDetailsViewModel subscribes to ObjMessage and once it receives the target TodoViewModel from ObjMessage's Payload - TodoDetailsViewModel updates itself accordingly.:-

 

C#
public class TodoDetailsViewModel: BaseViewModel
    {
        private TodoViewModel _CurrentTodo;
        IEventAggregator _eventAggregator;
        public ICommand GoBackCmd { get; private set; }
        private readonly ICommand _SaveTodoCmd;

        public ICommand SaveTodoCmd { get { return (_SaveTodoCmd); } }


        public TodoViewModel CurrentTodo
        {
            get { return _CurrentTodo; }
            set
            {
                if ((null != value) && (_CurrentTodo != value))
                {
                    _CurrentTodo = value;
                    OnPropertyChanged("CurrentTodo");
                }
            }
        }

        public TodoDetailsViewModel()
        {
            _eventAggregator = App.eventAggregator;
            _eventAggregator.Subscribe<mvvmcrudgv.views.objmessage>(UpdateTodo);

            //This goes in Initialization/constructor
            GoBackCmd = new RelayCommand(ExecGoBack, CanGoBack);
            _SaveTodoCmd = new RelayCommand(ExecSaveTodo, CanSaveTodo);
        }

        private void UpdateTodo(MvvmCrudGv.Views.ObjMessage message)
        {
            if(message.Notification.Equals("TodoDetails")){
            var td = (TodoViewModel)message.PayLoad;
            CurrentTodo = td;
            }

        }

        #region Commands

        private void ExecGoBack(object obj)
        {
            if (IsDirty)
            {
                System.Windows.MessageBoxResult confirmRunResult = System.Windows.MessageBox.Show("If you go back the changes will be discarded. Do you want to do this? If not, select 'Cancel' and 'Save' the changes first.", "Discard Changes?", System.Windows.MessageBoxButton.OKCancel);
                if (confirmRunResult == System.Windows.MessageBoxResult.Cancel)
                {
                    return;
                }
            }
            App.eventAggregator.Publish<views.navmessage>(new Views.NavMessage("Home"));
        }

        private bool CanGoBack(object obj)
        {
            return (true);
        }

        private void ExecSaveTodo(object obj)
        {
            //Todo: Add the functionality for SaveTodoCmd Here
            bool isok = BootStrapper.Instance.TodoService.Update(this.CurrentTodo._todo);
            IsDirty = !isok;
        }

        [DebuggerStepThrough]
        private bool CanSaveTodo(object obj)
        {
            //Todo: Add the checking for CanSaveTodo Here
            return (IsDirty);
        }
        #endregion
    }

</views.navmessage></mvvmcrudgv.views.objmessage>

(Back to Summary)

14a. This is our TodoPersistence layer's Contract and Implementation. These are both similar to our TodoService Contract and Implementation.  Just that these operate on our Protobuf-net Database - ProtobufDB.

C#
interface ITodoPersistence
    {
        Guid Add(Todo todo);
        void Delete(Guid id);
        List<Todo> List();
        bool Update(Todo todo);
        Todo Get(Guid id);
    }

class TodoPersistence: ITodoPersistence
    {
        AbstractCrudDB _protobufDb;

        public TodoPersistence()
        {
            _protobufDb = new ProtobufDB(MvvmCrudGvConstants.DefaultDataPath,"bin");
        }

        public Guid Add(Entity.Todo todo)
        {
            var filename = _protobufDb.Write<Todo>(todo, todo.Id.ToString());
            return (todo.Id);
        }

        public void Delete(Guid id)
        {
            _protobufDb.Delete<Todo>(id.ToString());
        }

        public List<Entity.Todo> List()
        {
            return (_protobufDb.Read<Todo>().ToList());
        }

        public bool Update(Entity.Todo todo)
        {
            //Nasty isn't it
            Guid id = Add(todo);
            return (true);
        }

        public Entity.Todo Get(Guid id)
        {
           return _protobufDb.Read<Todo>(id.ToString());
        }
    }

(Back to Summary)

14b. Here is our Persistence layer. I would not go into lots of details here. This basically contains very basic CRUD operations. Our Database is "Protobuf" database.  We are serializing our objects to "Protobuf" serialize objects.  Our ProdobufDb is basically using Protobuf-net to serialize/desrialize our objects to/from files. Nothing very fancy about it, rest are just helper classes which help the Database to do its duties.

C#
class ProtobufDB:AbstractCrudDB
    {
        private static readonly object syncLock = new object();

        public ProtobufDB(string basePath,string fileExtension):base(basePath,fileExtension)
        {

        }

        public override string Write<T>(T row, string id)
        {
            string filename = CreateFilename(typeof(T), id);

            lock (syncLock)
            {
                using (var file = File.Create(filename))
                {
                    Serializer.Serialize(file, row);
                }
            }

            return filename;
        }

        public override T Read<T>(string id)
        {
            string filename = CreateFilename(typeof(T), id);
            return readFileToType<T>(filename);
        }

        public override T[] Read<T>()
        {
            string filePattern = string.Format("{0}-*.{1}", typeof(T).Name, FileExtension);
            string[] files = Directory.GetFiles(BasePath, filePattern, SearchOption.TopDirectoryOnly);
            List<T> list = new List<T>();
            foreach (string filename in files)
            {
                list.Add(readFileToType<T>(filename));
            }
            return list.ToArray();
        }

        public override void Delete<T>(string id)
        {
            Delete(typeof(T), id);
        }

        public override void Delete(Type type, string id)
        {
            string filename = CreateFilename(type, id);
            if (File.Exists(filename))
            {
                File.Delete(filename);
            }
        }

        private T readFileToType<T>(string filename)
        {
            if (File.Exists(filename))
            {
                using (var file = File.OpenRead(filename))
                {
                    return (T)Serializer.Deserialize<T>(file);
                }
            }
            return default(T);
        }
    }


 abstract class AbstractCrudDB
    {
        protected AbstractCrudDB(string basePath,string fileExtension)
        {
            BasePath = basePath;
        }

        public string BasePath { get; private set; }
        public string FileExtension { get; private set; }

        //Create
        public abstract string Write<T>(T row, string id);

        //Read
        public abstract T Read<T>(string id);
        public abstract T[] Read<T>();

        //Delete
        public abstract void Delete<T>(string id);
        public abstract void Delete(Type type, string id);

        //Update
        //No update operation right now

        public virtual string CreateID()
        {
            return Guid.NewGuid().ToString("D");
        }

        protected string CreateFilename(Type type, string id)
        {
            return System.IO.Path.Combine(BasePath, string.Format("{0}-{1}.{2}", type.Name, id, FileExtension));
        }
    }

(Back to Summary)

14c. Utility Classes to help our Database side operations:-

C#
public class Utility
   {
       public static string getAbsolutePath(string folder, bool createIfNoDirectory = false)
       {
           string rtrnPath = Path.Combine(getAppBasePath(), folder);
           if ((createIfNoDirectory) && (!System.IO.Directory.Exists(folder)))
           {
               Directory.CreateDirectory(rtrnPath);
           }
           return (rtrnPath);
       }

       public static string getAbsolutePath(string folder, string fileName, bool createIfNoDirectory = false)
       {
           string rtrnPath = Path.Combine(getAppBasePath(), folder);

           if ((createIfNoDirectory) && (!System.IO.Directory.Exists(folder)))
           {
               Directory.CreateDirectory(rtrnPath);
           }
           rtrnPath = Path.Combine(rtrnPath, fileName);

           return (rtrnPath);
       }

       public static string getAppBasePath()
       {
           string codeBase = System.Reflection.Assembly.GetExecutingAssembly().Location;
           return Path.GetDirectoryName(codeBase);
       }
   }

   public struct MvvmCrudGvConstants
   {
       public static string DefaultLogFileName { get { return (Utility.getAbsolutePath("Log", "Error" + DateTimeStampAsString + ".log", true)); } }
       public static string DefaultLogFolder { get { return (Utility.getAbsolutePath("Log", true)); } }
       public static string DefaultDataPath { get { return (Utility.getAbsolutePath("Data", true)); } }
       public static string DefaultConfigPath { get { return (Utility.getAbsolutePath("Config", "MvvmCrudGv.cfg", true)); } }
       public static string DateTimeStampAsString { get { return (DateTime.Now.ToString("ddMMMyyyy_hhmm")); } }
   }

(Back to Summary)

Important Links:-

Weak Events memory leak issue

WPF-Validation

Mapping properties from view to view model

Styling Grid RowDefinitions and ColumnDefinitions

More to be added later.

History:

17-Oct-2014: Initial publish.
03-Nov-2014: Updated with updated Navigation and Navigation Messaging information.

 

 

License

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


Written By
Software Developer (Senior)
Singapore Singapore
I love programming, reading, and meditation. I like to explore management and productivity.

Comments and Discussions

 
GeneralMy vote of 3 Pin
Member 1130261022-Jan-16 0:44
Member 1130261022-Jan-16 0:44 
SuggestionTry to refer and implement Jeff Wilcox - Microsoft Silverlight and WPF: Sharing skills Video Pin
Satyasheel Dange2-Jun-15 4:51
professionalSatyasheel Dange2-Jun-15 4:51 
GeneralRe: Try to refer and implement Jeff Wilcox - Microsoft Silverlight and WPF: Sharing skills Video Pin
amitthk2-Jun-15 20:26
professionalamitthk2-Jun-15 20:26 
GeneralMessage Closed Pin
21-Nov-14 1:36
MahBulgaria21-Nov-14 1:36 
GeneralRe: My vote of 2 Pin
amitthk22-Nov-14 5:41
professionalamitthk22-Nov-14 5:41 
GeneralRe: My vote of 2 Pin
MahBulgaria24-Nov-14 1:11
MahBulgaria24-Nov-14 1:11 
GeneralRe: My vote of 2 Pin
amitthk24-Nov-14 7:01
professionalamitthk24-Nov-14 7:01 
Are you serious? You're expecting to see an explanation of Protobuf in an article of Wpf MVVM and rating for that? Protobuf is none of original intent of this article. You're stabbing the article ratings with a damaging rating of 2 for something which is totally out of context. If you want you can go ahead and store it in xml or binary files. People use several supporting tools to get their code working - what's wrong with that? Nobody is suggesting you that protobuf is any part of MVVM or wpf (or have I done that above?).

Moreover, the persistence code is the most simple CRUD persistence and it is Implementation on Contract so it is injectable. The code of article is working as suggested, it is not a broken code anyway.

If you have problem with Code or explanation of MVVM here, prove that you have a better solution. If you're capable of no constructive contribution, learn to understand the original intent of the article first. I don't like - I rate it 2 out of 5 - are you kidding me? I've noticed you playing juvenile delinquency on articles of other generous senior developers here.
GeneralRe: My vote of 2 Pin
MahBulgaria25-Nov-14 1:52
MahBulgaria25-Nov-14 1:52 
GeneralRe: My vote of 2 Pin
amitthk25-Nov-14 18:38
professionalamitthk25-Nov-14 18:38 
GeneralRe: My vote of 2 Pin
MahBulgaria25-Nov-14 23:01
MahBulgaria25-Nov-14 23:01 
GeneralRe: My vote of 2 Pin
amitthk25-Nov-14 23:41
professionalamitthk25-Nov-14 23:41 
GeneralRe: My vote of 2 Pin
MahBulgaria26-Nov-14 6:45
MahBulgaria26-Nov-14 6:45 
GeneralRe: My vote of 2 Pin
amitthk26-Nov-14 10:42
professionalamitthk26-Nov-14 10:42 
GeneralRe: My vote of 2 Pin
MahBulgaria27-Nov-14 0:26
MahBulgaria27-Nov-14 0:26 
GeneralRe: My vote of 2 Pin
amitthk27-Nov-14 15:33
professionalamitthk27-Nov-14 15:33 
GeneralRe: My vote of 2 Pin
#realJSOP19-Dec-14 2:32
mve#realJSOP19-Dec-14 2:32 
GeneralMy vote of 5 Pin
_Vitor Garcia_17-Oct-14 7:00
_Vitor Garcia_17-Oct-14 7:00 
GeneralRe: My vote of 5 Pin
amitthk17-Oct-14 7:15
professionalamitthk17-Oct-14 7:15 
GeneralMy vote of 4 Pin
Klaus Luedenscheidt16-Oct-14 19:16
Klaus Luedenscheidt16-Oct-14 19:16 
GeneralRe: My vote of 4 Pin
amitthk16-Oct-14 20:59
professionalamitthk16-Oct-14 20:59 
GeneralRe: My vote of 4 Pin
Klaus Luedenscheidt19-Oct-14 19:14
Klaus Luedenscheidt19-Oct-14 19:14 
GeneralRe: My vote of 4 Pin
amitthk19-Oct-14 20:12
professionalamitthk19-Oct-14 20:12 

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.