When the idea of doing this project came to me, at first I thought I would do it the same way as I did my previous WPF Writing one. I am doing this to simply to put into practice some of the concepts I have learnt previously. But using the same methods would not really prove much - at least to me. Since then I have learned much more and as such I wanted to use newer/updated tools, technologies and concepts to build this application.
Technologies and Concepts used in this project:
- Visual Studio 2008 SP1 + .NET Framework 3.5 SP1
- Microsoft Expression Blend 3 + Sketchflow
- Model-View-ViewModel (MVVM)
- Actions, Behaviors and Triggers
- Visual State Manager
- Data binding
- User Controls
- Styles
- Templates
The Prototype
Before an application is written, it may be helpful to have a decent, working prototype to sort of pitch/sell the idea of the project. Expression Blend 3 SketchFlow is the tool for the job. It will let you do rapid prototyping and provide a means to provide feedback on the initial prototype. My design will not be very complicated as this particular application will not have a whole lot of navigational needs. Basically the main window will show each element, a mouse over will bring up a summary for the element and clicking the control representing the element will bring up a details pane. Again, not a ton of interaction.
First, let's take a look at what the main UI may look like with a little bit of interaction in Fig. 1. Basically each element will have an attached GoToStateAction which will vary the Opacity of the SummaryUserControl. The states will react to the MouseEnter and MouseLeave events of the control.
Fig. 1. SketchFlow of the Main UI If the user wants detailed information on the particular element, then clicking the control that represents the element will navigate to a Details window as in Fig. 2.
Fig. 2. SketchFlow of the Details UI Fig. 3 is the SketchFlow map of the overall project. Notice that the MainUI screen has the SummaryUserControl Component linked to it and the DetailsUI has the Details Component linked to itself. The MainUI screen is also linked to the DetailsUI and the DetailsUI is linked back to the MainUI. As such you can use the NavigateForwardAction and NavigateBackAction to move between the screens.
Fig. 3. SketchFlow Map of the project The Design and Implementation
Fig. 4 is the Class Diagram of the overall application. Based on that, I will talk about each subsection as necessary. Fig. 5 is the hierarchy of the project.
Fig. 4. Class Diagram
Fig. 5. Project Hierarchy Given that my prototype passed and I have all the feedback necessary, then I would want to design next. This is where I would want to put some patterns and industry practices into play. I will use the Model-View-ViewModel pattern here, which I feel is appropriate for an application of this scale. Also, I want to use it to better acquaint myself with the pattern.
Model
The raw data would be an XML file that I have made up using the properties for the particular element. Since I have not used a lot of LINQ or XML processing in .NET, instead of using attributes and nested nodes, I have opted to use one level of nesting for all the nodes. Fig. 6 shows this.
Fig. 6. XML Data File The MVVM pattern completely separates the data from the rest of the application, so in order to expose the XML data, I have created a public class, ElementModel.cs whose sole purpose is to expose the data (nodes) of the XML data file as a set of public properties which can then be manipulated and data bound to. All this class does is declare a set of properties which mirror the data from the XML file. It supplies a single constructor that takes all these parameters in to create an instance of the class. In addition, there is a helper class called ElementModelFileLoader.cs which provides a single method that returns a List of ElementModel by using LINQ to load, parse and create the list. The List<ElementModel> can then be referenced by the ViewModel sections to be interacted with.
View
The MVVM kit will automatically create a MainView.xaml for a MVVM application. I will keep that view and add user controls and additional views as necessary. So the MainView will contain several ElementUserControl which will make up the periodic table. In addition, I have a second window called DetailsView.xaml which is loaded and displayed if the user clicks on an ElementUserControl. This window holds a DetailsUserControl.xaml. Both of these user controls use Data Binding to populate the necessary TextBlock fields that make them up.
Since I am on the topic, I have used Actions and Behaviors, some new features of Blend 3, as well as the Visual State Manager, to quickly add interactivity to my user controls. An Action is just a piece of code that runs after being triggered. For instance, Blend has a GoToStateAction which as its name implies, transitions the element it is attached to go to a specified state as defined by the VSM. For example, I have defined some states that will make the Border surrounding the ElementUserControl changes its Opacity property by listening for MouseEnter and MouseLeave events. See Fig. 7.
Fig. 7. Using Actions in Blend 3 I have an earlier blog post that has more information on Actions and Behaviors.
According to the MVVM pattern, a View will have a reference to its corresponding ViewModel, which it can set to be its data context ( myView.DataContext = myViewModelInstance ). In this way, bindings defined in the View, which references properties exposed by the ViewModel will be resolved. I will go into more detail when I discuss the ViewModels. Fig. 8 shows a representation of the MainView and Fig. 9 that of the DetailsView.
Fig. 8. The MainView view of the application
Fig. 9. The DetailsView view of the application ViewModel
Ideally, each of the Views listed above would have their own corresponding ViewModel file, which would be pure C# as shown in Fig. 5 above. Actually, I did not need a separate ViewModel for the DetailsView, but I added one one to bind to commands that is relevant to the DetailsView only. However, as of this writing, it is a redundant class as its functionality was not what I expected. I hope to fix it in the future.
Since most of the action happens in the MainViewModel, I will talk about that. Since this is what the View is bound to, it is the ViewModel that exposes properties and commands for the view to bind to. In this case, the MainViewModel wraps each List<ElementModel> into an ObservableCollection<ElementModelViewModel>. This may seem a bit confusion but the ElementModelViewModel is actually the ViewModel which represents the ElementModel. If you recall, the ElementModel was the class that exposes the data in the XML file. However, in the ElementModelViewModel you can choose exactly what properties you want to expose to your Views. For example, ElementModel could expose 10 properties, but out of those 10, the ElementModelViewModel class chooses only exposes 5 to its View. Since the View cannot "see" the model, then all it knows about is the 5 properties it is allowed to bind to.
In addition to custom object properties, the ViewModel also exposes Commands that the View can bind to. So in summary, the MainViewModel class exposes the properties of the Model as a collection and commands.
Since the code has lots of comments you can easily understand what is happening. A good deal of plumbing magic takes place in the OnStartup() event handler of the App code behind file - App.xaml.cs file. This is shown in Listing 1.
1: using System.Collections.ObjectModel;
2: using System.Windows;
3: using PeriodicTable.ViewModels;
4: using PeriodicTable.Views;
5:
6: namespace PeriodicTable
7: { 8: /// <summary>
9: /// Interaction logic for App.xaml
10: /// </summary>
11: public partial class App : Application
12: { 13: MainView view;
14: MainViewModel mainViewModel;
15: DetailsView detailsView;
16: DetailsViewViewModel detailsViewModel;
17:
18: public App()
19: { 20: view = new MainView();
21: mainViewModel = new MainViewModel();
22:
23: detailsView = new DetailsView();
24: detailsViewModel = new DetailsViewViewModel();
25:
26: }
27: private void OnStartup(object sender, StartupEventArgs e)
28: { 29: // Call the Load method in the MainViewModel call
30: // This finds the XML file, loads the data, create a CLR object
31: // to represent the XML data, create a collection of these
32: // objects and create a public property to expose this collection
33: mainViewModel.LoadElementsFromFile(@"Models\periodicTable.xml");
34:
35: ObservableCollection<ElementModelViewModel> col = mainViewModel.ElementCollection;
36:
37: // Since my user controls for the elements are not in a container
38: // that knows how to bind each element as an item in the container
39: // I have to manually iterate and set each control's data context property
40: // (ex: ListBox - if you set the ItemsSource to the property exposing
41: // the ObservableCollection, then each ListBoxItem is bound to
42: // one item in the collection)
43: for (int i = 0; i < col.Count; i++)
44: { 45: // use data binding to populate each user control
46: ElementUserControl euc = (ElementUserControl)view.elementsGrid.Children[i];
47: if (euc.Name == col[i].ElementName)
48: { 49: euc.DataContext = col[i];
50: euc.symbolText.Text = col[i].Symbol;
51: euc.atomicNumberText.Text = col[i].AtomicNumber;
52:
53: // respond to these events to do some bindings
54: euc.MouseEnter += new System.Windows.Input.MouseEventHandler(euc_MouseEnter);
55: euc.MouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler(euc_MouseLeftButtonDown);
56: }
57: }
58:
59: // set a higer level datacontext to get acccess to the Commands
60: // defined in the ViewModel
61: view.DataContext = mainViewModel;
62:
63: view.Show(); // show the main window
64: }
65:
66: // Handler for the MouseLeftButtonDown event which will show the DetailsView window
67: void euc_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
68: { 69: //detailsView.LayoutRoot.DataContext = (sender as ElementUserControl).DataContext;
70: detailsView.Owner = view;
71: if (detailsView == null)
72: { 73: detailsView = new DetailsView();
74: detailsView.Show();
75: }
76: else
77: { 78: detailsView.Show();
79: }
80: }
81:
82: // Handler for the MouseEnter event which set the data context of each
83: // both the Summary and Detail UserControls
84: void euc_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
85: { 86: view.summaryUC.DataContext = (sender as ElementUserControl).DataContext;
87: detailsView.LayoutRoot.DataContext = (sender as ElementUserControl).DataContext;
88: }
89: }
90: }
Listing 1: App code behind
In the constructor of the App class, instances of both the MainView and the DetailsView views are created and instantiated. Within the OnStartup() handler, the XML file is loaded, the collection of data is created and each ElementUserControl in the MainView has some of its compositional control values bound to properties of the objects in the collection of data items (Lines 43-51). The code is pretty much self explanatory.
Fig. 10 shows the overall operation of the application.
Fig. 10. The app in action