.NET & Funky Fresh

Syndication

News

  • <script type="text/javascript" src="http://ws.amazon.com/widgets/q?ServiceVersion=20070822&amp;MarketPlace=US&amp;ID=V20070822/US/bluspiconinc-20/8001/8b68bf4b-6724-40e7-99a5-a6decf6d8648"> </script>
Caliburn.Micro Soup to Nuts Part 6c – Simple MDI with Screen Collections

Let’s look at another example: this time a simple MDI shell that uses “Screen Collections.” As you can see, once again, I have kept things pretty small and simple:

SimpleMdiProject

Here’s a screenshot of the application when it’s running:

SimpleMdiScreenshot

 

Here we have a simple WPF application with a series of tabs. Clicking the “Open Tab” button does the obvious. Clicking the “X” inside the tab will close that particular tab (also, probably obvious). Let’s dig into the code by looking at our ShellViewModel:

public class ShellViewModel : Conductor<IScreen>.Collection.OneActive {
    int count = 1;

    public void OpenTab() {
        ActivateItem(new TabViewModel {
            DisplayName = "Tab " + count++
        });
    }
}

Since we want to maintain a list of open items, but only keep one item active at a time, we use Conductor<T>.Collection.OneActive as our base class. Note that, different from our previous example, I am actually constraining the type of the conducted item to IScreen. There’s not really a technical reason for this in this sample, but this more closely mirrors what I would actually do in a real application. The OpenTab method simply creates an instance of a TabViewModel and sets its DisplayName property (from IScreen) so that it has a human-readable, unique name. Let’s think through the logic for the interaction between the conductor and its screens in several key scenarios:

Opening the First Item

  1. Adds the item to the Items collection.
  2. Checks the item for IActivate and invokes it if present.
  3. Sets the item as the ActiveItem.

Opening an Additional Item

  1. Checks the current ActiveItem for IDeactivate and invokes if present. False is passed to indicate that it should be deactivated only, and not closed.
  2. Checks the new item to see if it already exists in the Items collection. If not, it is added to the collection. Otherwise, the existing item is returned.
  3. Checks the item for IActivate and invokes if present.
  4. Sets the new item as the ActiveItem.

Closing an Existing Item

  1. Passes the item to the CloseStrategy to determine if it can be closed (by default it looks for IGuardClose). If not, the action is cancelled.
  2. Checks to see if the closing item is the current ActiveItem. If so, determine which item to activate next and follow steps from “Opening an Additional Item.”
  3. Checks to see if the closing item is IDeactivate. If so, invoke with true to indicate that it should be deactivated and closed.
  4. Remove the item from the Items collection.

Those are the main scenarios. Hopefully you can see some of the differences from a Conductor without a collection and understand why those differences are there. Let’s see how the ShellView renders:

<Window x:Class="Caliburn.Micro.SimpleMDI.ShellView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cal="http://www.caliburnproject.org"
        Width="640"
        Height="480">
    <DockPanel>
        <Button x:Name="OpenTab"
                Content="Open Tab" 
                DockPanel.Dock="Top" />
        <TabControl x:Name="Items">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding DisplayName}" />
                        <Button Content="X"
                                cal:Message.Attach="CloseItem($dataContext)" />
                    </StackPanel>
                </DataTemplate>
            </TabControl.ItemTemplate>
        </TabControl>
    </DockPanel>
</Window>

As you can see we are using a WPF TabControl. CM’s conventions will bind its ItemsSource to the Items collection and its SelectedItem to the ActiveItem. It will also add a default ContentTemplate which will be used to compose in the ViewModel/View pair for the ActiveItem. Conventions can also supply an ItemTemplate since our tabs all implement IHaveDisplayName (through Screen), but I’ve opted to override that by supplying my own to enable closing the tabs. We’ll talk more in depth about conventions in a later article. For completeness, here are the trivial implementations of TabViewModel along with its view:

namespace Caliburn.Micro.SimpleMDI {
    public class TabViewModel : Screen {}
}
<UserControl x:Class="Caliburn.Micro.SimpleMDI.TabView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="This is the view for "/>
        <TextBlock x:Name="DisplayName" />
        <TextBlock Text="." />
    </StackPanel>
</UserControl>

I’ve tried to keep it simple so far, but that’s not the case for our next sample. In preparation, you might want to at least think through or try to do these things:

  • Get rid of the generic TabViewModel. You wouldn’t really do something like this in a real application. Create a couple of custom view models and views. Wire things up so that you can open different view models in the conductor. Confirm that you see the correct view in the tab control when each view model is activated.
  • Rebuild this sample in Silverlight. Unfortunately, Silverlight’s TabControl is utterly broken and cannot fully leverage databinding. Instead, try using a horizontal ListBox as the tabs and a ContentControl as the tab content. Put these in a DockPanel and use some naming conventions and you will have the same affect as a TabControl.
  • Create a toolbar view model. Add an IoC container and register the ToolBarViewModel as a singleton. Add it to the ShellViewModel and ensure that it is rendered in the ShellView (remember you can use a named ContentControl for this). Next, have the ToolBarViewModel injected into each of the TabViewModels. Write code in the TabViewModel OnActivate and OnDeactivate to add/remove contextual items from the toolbar when the particular TabViewModel is activated. BONUS: Create a DSL for doing this which doesn’t require explicit code in the OnDeactivate override. HINT: Use the events.
  • Take the SimpleMDI sample and the SimpleNavigation sample and compose them together. Either add the MDI Shell as a PageViewModel in the Navigation Sample or add the Navigation Shell as a Tab in the MDI Sample.

Posted 10-19-2010 4:51 PM by Rob Eisenberg

[Advertisement]

Comments

Calin wrote re: Caliburn.Micro Soup to Nuts Part 6c – Simple MDI with Screen Collections
on 10-20-2010 2:11 AM

Hi Rob,

Very nice post I was looking forward it, even done the exercises you proposed but I am little stuck on this part:

"Create a DSL for doing this which doesn’t require explicit code in the OnDeactivate override"

I don't really understand the DSL term in this context. A hint will be great.

Thank you,

Rob Eisenberg wrote re: Caliburn.Micro Soup to Nuts Part 6c – Simple MDI with Screen Collections
on 10-20-2010 9:09 AM

Imagine a little C# internal DSL for your toolbar...something like this inside your screen:

toolbar.ScopedTo(this)

  .HasButton("Save").Executing(SomeAction)

  .HasCombo().WithItems(BuildList()).OnChange(AnotherAction)

  .HasText().OnChange(AnotherAction)

This would add the items to the toolbar when the Activate event fires and remove them when the Deactivated event fires. There's lots of ways to accomplish this.

Dietrich wrote re: Caliburn.Micro Soup to Nuts Part 6c – Simple MDI with Screen Collections
on 10-22-2010 2:23 PM

Very helpful post!

Unfortunately I'm totally lost on the third "Optional" item. I'm Looking forward to your next post explaining them.

JJ wrote re: Caliburn.Micro Soup to Nuts Part 6c – Simple MDI with Screen Collections
on 11-17-2010 7:00 PM

When I run this project it renders but I'm getting a MissingMethodException "Cannot create an instance of an interface." It appears to be in this t/c:

  IWindowManager windowManager;

           try

           {

               windowManager = IoC.Get<IWindowManager>();

           }

           catch

           {

               windowManager = new WindowManager();

           }

           windowManager.Show(viewModel);

Rob Eisenberg wrote re: Caliburn.Micro Soup to Nuts Part 6c – Simple MDI with Screen Collections
on 11-17-2010 10:06 PM

Continue through the exception, it should be caught below and continue to work.

About The CodeBetter.Com Blog Network
CodeBetter.Com FAQ

Our Mission

Advertisers should contact Brendan

Subscribe
Google Reader or Homepage

del.icio.us CodeBetter.com Latest Items
Add to My Yahoo!
Subscribe with Bloglines
Subscribe in NewsGator Online
Subscribe with myFeedster
Add to My AOL
Furl CodeBetter.com Latest Items
Subscribe in Rojo

Member Projects
DimeCasts.Net - Derik Whittaker

Friends of Devlicio.us
Red-Gate Tools For SQL and .NET

NDepend

SlickEdit
 
SmartInspect .NET Logging
NGEDIT: ViEmu and Codekana
LiteAccounting.Com
DevExpress
Fixx
NHibernate Profiler
Unfuddle
Balsamiq Mockups
Scrumy
JetBrains - ReSharper
Umbraco
NServiceBus
RavenDb
Web Sequence Diagrams
Ducksboard<-- NEW Friend!

 



Site Copyright © 2007 CodeBetter.Com
Content Copyright Individual Bloggers

 

Community Server (Commercial Edition)