Archive for November, 2009

WPF: Replicating MDI Application Behaviour

November 25, 2009

I decided to use Prism, aka Microsoft Composite Application Guidance For WPF and Silverlight.

This is very much like using a supersonic jet fighter to pop down to the local shops but its effective, not very hard and does not require you to write reams of fragile custom code. You just need to spend a bit of time getting used to Prism. Now that’s not a trivial task, but you should have covered the fundamental concepts within a working week, assuming you have some prior knowledge of Dependency Injection.

The huge advantage to you is that Prism is written by people who are much better programmers than you or I are, so the code is robust, whereas writing a custom MDI Window Manager is out of the league of most Intermediate-level developers (most of us).

Very simply, Prism allows the development of Modular Applications in which the User Interface of an application is decomposed into loosely-coupled Modules, each of which occupies a specific area on the screen, known as a Region.

No window (known in Prism as a View) can move outside the Region in which it is contained. This replicates the MDI-like behaviour you’re looking for.

Prism is also a library of Best Practices – it’s produced by the Microsoft Patterns and Practices team – so you get heaps of built-ins for free. And there’s a helpful forum where others using Prism are happy to help us newbies.

You can also have multiple windows (views) open simultaneously if you want, but this will render as a tabbed interface which is not exactly the same as MDI.

I embarked upon this approach and have been successful in producing a MDI-like app. It wasn’t too hard and I learnt heaps along the way. Go for it.

Update 27-Jan-2010

Ahmed in comments asked me to post the code for my MDI sample.

All I did was use a WPF ContentControl for each Prism Region. A ContentControl can only display one View at any one time, so each time you activate a View the one currently in the Region becomes invisible and the new one you activated becomes visible.

This provides the illusion that there is only one window present at any one time. In fact, all the Views you activated are still there, just invisible until you explictly RegionManager.Remove the View from the Region. This is kind of good though, because the ‘invisible’ views are there in memory and so become visible again instantly you do RegionManager.Activate on them. Effectively this is caching the Views for performance.

So here’s my Shell Window markup:

Grid/
ContentControl Grid.Column="1" Grid.Row="1" cal:RegionManager.RegionName="MainRegion"
VerticalContentAlignment="Top"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"

ContentControl Grid.Column="1" Grid.Row="1" Name="AdminRegion"
cal:RegionManager.RegionName="AdminRegion"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
Grid.ColumnSpan="2"

ContentControl Grid.Column="2" Grid.Row="1" cal:RegionManager.RegionName="ToDoListRegion"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"

/Grid

So you notice I have three ContentControls, one for each of my regions. I also have a Menu with a gazillion MenuItems (not shown).

Each Menu Item is bound to a Prism DelegateCommand like so:

'''
''' Initialise Commands On The Module Menu
'''
Private Sub InitialiseContentCommands()
Me.ContentShowLocalContentCommand = _
New DelegateCommand(Of Object)(AddressOf Me.ShowLocalContent)
Me.ContentShowModuleContentCommand = _
New DelegateCommand(Of Object)(AddressOf Me.ShowModuleContent)
Me.ContentShowModuleDetailsCommand = _
New DelegateCommand(Of Object)(AddressOf Me.ShowModuleDetails)
End Sub

And here’s the ViewModel code related to those DelegateCommands to show the View:

#Region "Module View Injection..."

'''
''' Show Module Content View
''' Update MainRegion with Module Content View
'''
Public Sub ShowModuleContent()
Dim mainRegion As Region = mainView._regionManager.Regions(RegionName.MainRegion)

Dim existingView As Object = mainRegion.GetView(ViewName.ModuleContentView)

If (existingView Is Nothing) Then

Dim moduleContentViewModel As IModuleContentViewModel = _
mainView._container.Resolve(Of IModuleContentViewModel)()
mainRegion.Add(moduleContentViewModel.View, ViewName.ModuleContentView)
mainRegion.Activate(moduleContentViewModel.View)
Else
mainRegion.Activate(existingView)
End If

End Sub

#End Region

Also in the Shell ViewModel I handle a Prism CompositeEvent that is published every time a View is closed in the MainRegion (i.e. by the User clicking on the Close Button for a View. This removes (destroys) the View as opposed to just letting it get overwritten by the next View that gers activated.

#Region "Event Handlers..."

'''
''' Handler for Composite CloseViewEvent
'''
Public Sub CloseViewEventHandler(ByVal closeViewInfo As CloseViewInfo)
Dim regionManager As RegionManager = _
mainView._container.Resolve(Of IRegionManager)()

'Find the region
Dim regionQuery = _
From regn In regionManager.Regions _
Where regn.Name = closeViewInfo.RegionName _
Select regn

' Find the View in the region
Dim region As Region = regionQuery.Single()
Dim view As Object = region.GetView(closeViewInfo.ViewName)

' Remove the view
If view IsNot Nothing Then
region.Remove(view)
End If

' Since ContentControl can only show one view at a time, if any one View is closed
' then no views are visible
Me.AllViewsClosed()
End Sub

I also have a CloseAll MenuItem which closes all the cached (invisible) Views in the Main Region, to give the users and the application the opportunity to clean up all those Views. This is done by having every View Register its CloseButton Command with the a Prism CompositeCommand called CloseAllCommand and having every View provides a handler which Publishes a Prism Event Aggregation event I defined called CloseView.

The Shell ViewModel subscribes to the Event Aggregation CloseView event and removes the calling View from the MainRegion. Since all Views do this, all Views are Removed

In XAML:

MenuItem Header=”_ Close Background Windows” Name=”mniCloseAll”
Command=”{x:Static inf:GlobalCommand.CloseAllCommand}” FontSize=”9″ /

In the ViewModel:

”’
”’ Subscribe to Event Aggregator CloseViewEvent
”’
”’
Private Sub SubscribeCloseViewEvent()

Dim ea As EventAggregator = mainView._container.Resolve(Of IEventAggregator)()

‘ Subscribe to CloseViewEvent
Dim closeViewevent As CloseViewEvent = ea.GetEvent(Of CloseViewEvent)()

If _closeViewSubscriptionToken IsNot Nothing Then
closeViewevent.Unsubscribe(_closeViewSubscriptionToken)
End If

‘ Perform the event on the UIThread
‘ keepSubscriberReference Alive is false. This means the event maintains a
‘ weak reference to the subscriber instance, allowing the garbage collector to
‘ dispose the subscriber instance when there are no other references to it.
‘ When the subscriber instance gets collected, the event automatically unsubscribes.
_closeViewSubscriptionToken = _
closeViewevent.Subscribe(AddressOf CloseViewEventHandler, ThreadOption.UIThread, False)
End Sub

So hopefully you can see that each region only has one View visible at any one time (kind of like a MDI application that I am porting) and because the Views are situated in a Region, they can’t move out of there, which is MDI-like behaviour. No letting your users drag windows all over the screen and losing them.

Careless users.

Visual Studio 2008 Disappears From Screen When Loading Solution or Project

November 5, 2009

I started up Visual Studio 2008 and clicked on my Solution file AmazingSolution on the Start Page. Visual Studio started dragging up the solution and was just loading up a XAML file when it disappeared. Just disappeared.

This was about as expected as a nasty case of Sponteneous Combustion. Alice In Wonderland is one of my favourite books but when Visual Studio starts behaving like the Chesire Cat it’s a little perturbing…more so since I had just hacked the Registry to overcome the issue of Intellisense not appearing in XAML files after installation of Windows SDK.

My terror was complete. I had destroyed Visual Studio.

The paramedics were called, the Emergency Sacrifical Ferret put on stand-by and I ran a repair on Visual Studio, as Microsoft Connect recommended.

Then I rebooted, restarted Visual Studio and loaded AmazingProject. Visual Studio disappeared again. I slaughtered the Ferret and ordered a crate of Vodka in celebration of my impending sacking, but then checked the Event Logs. Glory! It contained a very useful entry:

.NET Runtime version 2.0.50727.3603 – Fatal Execution Engine Error (7A036050) (80131506)

and Googling that showed that Microsoft Connect have patch files for that very issue…Well, it was too late for the Ferret, but I HADN”T DESTROYED VISUAL STUDIO AFTER ALL!

To Summarize

1. The horrifying sudden disappearance of Visual Studio 2008 can be caused by a bug relating to the loading of XAML files. Check your Event Logs and if you see .NET Runtime version 2.0.50727.3603 – Fatal Execution Engine Error get the patch from Microsoft Connect at the link above. No Ferrets need to be slaughtered.

2. The unrelated problem of Intellisense disappearing from XAML files is caused by installing a Windows SDK after (on top of) Visual Studio 2008. This overwrites the registry key VS uses to locate the XAML Intellisense file. To fix that, run a Repair on Visual Studio 2008 or, if you’re feeling brave, hack the registry.

So What Happened To You ?

Sick of having no Intellisense in XAML I Googled the registry hack. At the same time I did a ‘Close All But Me’ in Visual Studio on a XAML file then closed Visual Studio. I then restarted Visual Studio to allow the registry hack to take effect, whereupon it did the Chesire Cat on me. It was simply a malevolent coincidence…perhaps a warning never to do a registry hack when a more polite known fix exists 🙂 (In this case the Visual Studio Repair.

Hey Bruce, chuck us another Ferret will ya ?

WPF Setting Default Sort Order For Datagrid Without Using A Static Resource

November 4, 2009

For this you need a CollectionViewSource and bind the DataGrid ItemsSource to the View property of the CollectionViewSource.

The Basic Wire-Up
Andrea Boschin on Silverlight PlayGround demonstrates the basic wiring for Binding the DataGrid ItemsSource to the CollectionViewSource. Andrea is using MVVM and hence the CollectionViewSource is a property in his ViewModel class. I’ve excerpted it here:

Public Class SomeViewModel

// The View of this cvs will get bound to a datagrid
Public CollectionViewSource People { get; set; }

// The actual data that the cvs will construct its view out of
protected ObservableCollection(Of People) PeopleInternal { get; set; }
}
} //end class

In XAML, bind the DataGrid to the View of the People CollectionViewSource:

wpftoolkit:DataGrid ItemsSource="{Binding People.View}"
wpftoolkit:DataGrid.Columns
wpftoolkit:DataGridTextColumn Header="Name"
MinWidth="120" Binding="{Binding Path=Name, Mode=TwoWay}" /
wpftoolkit:DataGridTextColumn Header="Age"
Width="auto" Binding="{Binding Path=Age, Mode=TwoWay}" /
/dg:DataGrid.Columns
/wpftoolkit:DataGrid

Taking over from where Andrea left off, when the ObservableCollection is set, we initialise the CollectionViewSource:


protected ObservableCollection(Of People) PeopleInternal
{ get {return _peopleInternal };
set;
{
_peopleInternal = value
People.Source = _peopleInternal
}
}

Setting Default Sort Order
In order to establish the default Sort Order for the Datagrid, add a SortDescription to the CollectionViewSource that the DataGrid is bound to. This creates the required view.

Let’s say we wanted our Datagrid to have a sort order of ‘Name’, assuming ‘Name’ is a field on the People class.


protected ObservableCollection(Of People) PeopleInternal
{ get {return _peopleInternal };
set;
{
_peopleInternal = value
People.Source = _peopleInternal
// Setting Sort Order for People
// Sort by Name field
People.SortDescriptions.Clear()
SortDescription sd = new SortDescription("Name", ListSortOrder.Ascending)
People.SortDescriptions.Add(sd)
}
}

So, every time PeopleInternal is updated, the CollectionViewSource creates a view on PeopleInternal which is sorted by Name. This is held in People.View. Because the DataGrid is bound to this View the data in it is always sorted by name when the ItemsSource (the ObservableCollection(Of People) is initialised

This works around the behaviour in the WPF Toolkit DataGrid where all SortDescriptions for the DataGrid are lost when the ItemsSource is initialised

Of course, users will still be able to click on Column Headers and sort at any time on any other column they want to.