Programming Silverlight with MVVM pattern using Prism
Here’s what I wanted to do:
- A Silverlight project
- User logs in
- A service runs and displays data upon successful login
Quite simple right? In ‘pure’ Silverlight, one could probably do this in like 15 minutes, but I wanted to do this using Prism following the MVVM (Model-View-ViewModel) pattern. I added a couple more conditions:
- The main control of the Silverlight application is a TabControl with Login and Data as TabItems
- The first screen is the login screen
- When the login screen is displayed, the Data tabitem should not be displayed as it would not contain any data (information gets to the Data tabitem only after successful login) and vice versa
I don’t have any intentions of writing this blog to develop the whole application; I’ll detail only on the key concepts (the additional conditions).
According to me, adding Login and Data UI screens as items of a TabControl would be the easiest of them all. I thought, it’s only a bunch of data bindings showing Header and Content information. To a good extent it was, except for a catch (there always is one!!). The TabControl is not completely dependable when using with Prism in the sense that it does not allow dynamic binding of the HeaderTemplate (to show the header information on the tab). Here's where the issue is being tracked.
There are workarounds for this that involve modifying the source code. The one that worked for me was to add a couple of lines to the TabControlRegionAdapter.cs class in the PrepareContainerForItem method. The modified version of the method looks like below (added lines 10-13):
1: private static TabItem PrepareContainerForItem(object item, DependencyObject parent)
2: {
3: TabItem container = item as TabItem;
4: if (container == null)
5: {
6: container = new TabItem();
7: container.Content = item;
8: container.DataContext = GetDataContext(item);
9: container.Style = GetItemContainerStyle(parent);
10: if (container.HeaderTemplate != null)
11: {
12: container.Header = container.HeaderTemplate.LoadContent();
13: }
14: container.SetValue(IsGeneratedProperty, true);
15: }
16:
17: return container;
18: }
I compiled the project to Release mode and used the Microsoft.Practices.Composite.Presentation.dll in my project. Now my shell.xaml code looks like:
1: <Controls:TabControl
2: Regions:RegionManager.RegionName="ControlsRegion"
3: Grid.Row="1" Margin="3">
4: <Regions:TabControlRegionAdapter.ItemContainerStyle>
5: <Style TargetType="Controls:TabItem">
6: <Setter Property="HeaderTemplate">
7: <Setter.Value>
8: <DataTemplate>
9: <TextBlock
10: Text="{Binding HeaderInfo}"
11: FontSize="12" VerticalAlignment="Center" />
12: </DataTemplate>
13: </Setter.Value>
14: </Setter>
15: </Style>
16: </Regions:TabControlRegionAdapter.ItemContainerStyle>
17: </Controls:TabControl>
See line 10: I have a ‘HeaderInfo’ property in my viewmodel to which the view binds to and displays the information.
On to the second condition: All I did to accomplish this was to register only the ‘LoginView’ when the application ran for the first time. This was contrary to what is being done in most webcasts and blog articles – registering all views in the Instantiate method of the module class. My approach was to load the second view only if the passwords matched.
The third one was a little tricky – to make the Login and the Data views mutually exclusive. Put in Prism language, this translated to: register the Data view with the region only if the password matches. In the same block of code, I needed to remove the already registered ‘LoginView’ from my main region.
To achieve part 1, I added the following line to the OnLogin method in the LoginViewModel class. This will register the ‘Data’ view to the main region.
1: RegionManager.RegisterViewWithRegion("ControlsRegion", typeof (MetricsView));
(Sorry for the bad naming convention, it’s because I’ll be continuing this project to gather some metrics information from my website).
In order to remove the LoginView from the main region, I resorted to using the following code:
1: private void RemoveView(string viewFullName)
2: {
3: // Get all views that are using MainRegion
4: var mainRegion = RegionManager.Regions["ControlsRegion"];
5:
6: // Remove the view with a matching name
7: foreach (var view in mainRegion.Views)
8: {
9: var viewType = view.GetType();
10: if (viewType.FullName != viewFullName) continue;
11: mainRegion.Remove(view);
12: break;
13: }
14: }
There it is, that does it - removes the view name that was passed to it from the main region.
Like many things in programming, there are other ways of doing what I’ve done above. I have posted the entire solution here.