.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 6d – A “Billy Hollis” Hybrid Shell

HelloScreensSolutionUp until now I’ve been focusing on fairly simple usage of Screens and Conductors. In this article, I want to show something a bit more sophisticated. This sample is based loosely on the ideas demonstrated by Billy Hollis in this well-known DNR TV episode.  Rather than take the time to explain what the UI does, have a look at this short video for a brief visual explanation (apologies for the audio level).

Ok, now that you’ve seen what it does, let’s look at how it’s put together. As you can see from the screenshot, I’ve chosen to organize the project by features: Customers, Orders, Settings, etc. In most projects I prefer to do something like this rather than organizing by “technical” groupings, such as Views and ViewModels. If I have a complex feature, then I might break that down into those areas.

I’m not going to go line-by-line through this sample. It’s better if you take the time to look through it and figure out how things work yourself. But, I do want to point out a few interesting implementation details.

 

ViewModel Composition

One of the most important features of Screens and Conductors in Caliburn.Micro is that they are an implementation of the Composite Pattern, making them easy to compose together in different configurations. Generally speaking, composition is one of the most important aspects of object oriented programming and learning how to use it in your presentation tier can yield great benefits. To see how composition plays a role in this particular sample, lets look at two screenshots. The first shows the application with the CustomersWorkspace in view, editing a specific Customer’s Address. The second screen is the same, but with its View/ViewModel pairs rotated three-dimensionally, so you can see how the UI is composed.

Editing a Customer’s Address

Composition

Editing a Customer’s Address (3D Breakout)

Composition-3d-01

In this application, the ShellViewModel is a Conductor<IWorkspace>.Collection.OneActive. It is visually represented by the Window Chrome, Header and bottom Dock. The Dock has buttons, one for each IWorkspace that is being conducted. Clicking on a particular button makes the Shell activate that particular workspace. Since the ShellView has a TransitioningContentControl bound to the ActiveItem, the activated workspace is injected and it’s view is shown at that location. In this case, it’s the CustomerWorkspaceViewModel that is active.  It so happens that the CustomerWorkspaceViewModel inherits from Conductor<CustomerViewModel>.Collection.OneActive. There are two contextual views for this ViewModel (see below). In the screenshot above, we are showing the details view. The details view also has a TransitioningContentControl bound to the CustomerWorkspaceViewModel’s ActiveItem, thus causing the current CustomerViewModel to be composed in along with its view. The CustomerViewModel has the ability to show local modal dialogs (they are only modal to that specific custom record, not anything else). This is managed by an instance of DialogConductor, which is a property on CustomerViewModel. The view for the DialogConductor overlays the CustomerView, but is only visible (via a value converter) if the DialogConductor’s ActiveItem is not null. In the state depicted above, the DialogConductor’s ActiveItem is set to an instance of AddressViewModel, thus the modal dialog is displayed with the AddressView and the underlying CustomerView is disabled. The entire shell framework used in this sample works in this fashion and is entirely extensible simply by implementing IWorkspace. CustomerViewModel and SettingsViewModel are two different implementations of this interface you can dig into.

 

Multiple Views over the Same ViewModel

You may not be aware of this, but Caliburn.Micro can display multiple Views over the same ViewModel. This is supported by setting the View.Context attached property on the View/ViewModel’s injection site. Here’s an example from the default CustomerWorkspaceView:

 

<clt:TransitioningContentControl cal:View.Context="{Binding State, Mode=TwoWay}"
                                 cal:View.Model="{Binding}" 
                                 Style='{StaticResource specialTransition}'/>

 

There is a lot of other Xaml surrounding this to form the chrome of the CustomerWorkspaceView, but the content region is the most noteworthy part of the view. Notice that we are binding the View.Context attached property to the State property on the CustomerWorkspaceViewModel. This allows us to dynamically change out views based on the value of that property. Because this is all hosted in the TransitioningContentControl, we get a nice transition whenever the view changes. This technique is used to switch the CustomerWorkspaceViewModel from a “Master” view, where it displays all open CustomerViewModels, a search UI and a New button, to a “Detail” view, where it displays the currently activated CustomerViewModel along with it’s specific view (composed in). In order for CM to find these contextual views, you need a namespace based on the ViewModel name, minus the words “View” and “Model”, with some Views named corresponding to the Context. For example, when the framework looks for the Detail view of Caliburn.Micro.HelloScreens.Customers.CustomersWorkspaceViewModel, it’s going to look for Caliburn.Micro.HelloScreens.Customers.CustomersWorkspace.Detail That’s the out-of-the-box naming convention. If that doesn’t work for you, you can simply customize the ViewLocator.LocateForModelType func.

 

Custom IConductor Implementation

Although Caliburn.Micro provides the developer with default implementations of IScreen and IConductor. It’s easy to implement your own. In the case of this sample, I needed a dialog manager that could be modal to a specific part of the application without affecting other parts. Normally, the default Conductor<T> would work, but I discovered I needed to fine-tune shutdown sequence, so I implemented my own. Let’s take a look at that:

[Export(typeof(IDialogManager)), PartCreationPolicy(CreationPolicy.NonShared)]
public class DialogConductorViewModel : PropertyChangedBase, IDialogManager, IConductor {
    readonly Func<IMessageBox> createMessageBox;

    [ImportingConstructor]
    public DialogConductorViewModel(Func<IMessageBox> messageBoxFactory) {
        createMessageBox = messageBoxFactory;
    }

    public IScreen ActiveItem { get; private set; }

    public IEnumerable GetConductedItems() {
        return ActiveItem != null ? new[] { ActiveItem } : new object[0];
    }

    public void ActivateItem(object item) {
        ActiveItem = item as IScreen;

        var child = ActiveItem as IChild<IConductor>;
        if(child != null)
            child.Parent = this;

        if(ActiveItem != null)
            ActiveItem.Activate();

        NotifyOfPropertyChange(() => ActiveItem);
        ActivationProcessed(this, new ActivationProcessedEventArgs { Item = ActiveItem, Success = true });
    }

    public void CloseItem(object item) {
        var guard = item as IGuardClose;
        if(guard != null) {
            guard.CanClose(result => {
                if(result)
                    CloseActiveItemCore();
            });
        }
        else CloseActiveItemCore();
    }

    object IConductor.ActiveItem {
        get { return ActiveItem; }
        set { ActivateItem(value); }
    }

    public event EventHandler<ActivationProcessedEventArgs> ActivationProcessed = delegate { };

    public void ShowDialog(IScreen dialogModel) {
        ActivateItem(dialogModel);
    }

    public void ShowMessageBox(string message, string title = null, MessageBoxOptions options = MessageBoxOptions.Ok, Action<IMessageBox> callback = null) {
        var box = createMessageBox();

        box.DisplayName = title ?? "Hello Screens";
        box.Options = options;
        box.Message = message;

        if(callback != null)
            box.Deactivated += delegate { callback(box); };

        ActivateItem(box);
    }

    void CloseActiveItemCore() {
        var oldItem = ActiveItem;
        ActivateItem(null);
        oldItem.Deactivate(true);
    }
}

 

Strictly speaking, I didn’t actually need to implement IConductor to make this work (since I’m not composing it into anything). But I chose to do this in order to represent the role this class was playing in the system and keep things as architecturally consistent as possible. The implementation itself is pretty straight forward. Mainly, a conductor needs to make sure to Activate/Deactivate its items correctly and to properly update the ActiveItem property. I also created a couple of simple methods for showing dialogs and message boxes which are exposed through the IDialogManager interface. This class is registered as NonShared with MEF so that each portion of the application that wants to display local modals will get its own instance and be able to maintain its own state, as demonstrated with the CustomerViewModel discussed above.

 

Custom ICloseStrategy

Possibly one of the coolest features of this sample is how we control application shutdown. Since IShell inherits IGuardClose, in the Bootstrapper we just override DisplayRootView and wire Silverlight’s MainWindow.Closing event to call IShell.CanClose:

protected override void DisplayRootView() {
    base.DisplayRootView();

    if (Application.IsRunningOutOfBrowser) {
        mainWindow = Application.MainWindow;
        mainWindow.Closing += MainWindowClosing;
    }
}

void MainWindowClosing(object sender, ClosingEventArgs e) {
    if (actuallyClosing)
        return;

    e.Cancel = true;

    Execute.OnUIThread(() => {
        var shell = IoC.Get<IShell>();

        shell.CanClose(result => {
            if(result) {
                actuallyClosing = true;
                mainWindow.Close();
            }
        });
    });
}

The ShellViewModel inherits this functionality through its base class Conductor<IWorkspace>.Collection.OneActive. Since all the built-in conductors have a CloseStrategy, we can create conductor specific mechanisms for shutdown and plug them in easily. Here’s how we plug in our custom strategy:

[Export(typeof(IShell))]
public class ShellViewModel : Conductor<IWorkspace>.Collection.OneActive, IShell
{
    readonly IDialogManager dialogs;

    [ImportingConstructor]
    public ShellViewModel(IDialogManager dialogs, [ImportMany]IEnumerable<IWorkspace> workspaces) {
        this.dialogs = dialogs;
        Items.AddRange(workspaces);
        CloseStrategy = new ApplicationCloseStrategy();
    }

    public IDialogManager Dialogs {
        get { return dialogs; }
    }
}

And here’s the implementation of that strategy:

public class ApplicationCloseStrategy : ICloseStrategy<IWorkspace> {
    IEnumerator<IWorkspace> enumerator;
    bool finalResult;
    Action<bool, IEnumerable<IWorkspace>> callback;

    public void Execute(IEnumerable<IWorkspace> toClose, Action<bool, IEnumerable<IWorkspace>> callback) {
        enumerator = toClose.GetEnumerator();
        this.callback = callback;
        finalResult = true;

        Evaluate(finalResult);
    }

    void Evaluate(bool result)
    {
        finalResult = finalResult && result;

        if (!enumerator.MoveNext() || !result)
            callback(finalResult, new List<IWorkspace>());
        else
        {
            var current = enumerator.Current;
            var conductor = current as IConductor;
            if (conductor != null)
            {
                var tasks = conductor.GetConductedItems()
                    .OfType<IHaveShutdownTask>()
                    .Select(x => x.GetShutdownTask())
                    .Where(x => x != null);

                var sequential = new SequentialResult(tasks.GetEnumerator());
                sequential.Completed += (s, e) => {
                    if(!e.WasCancelled)
                    Evaluate(!e.WasCancelled);
                };
                sequential.Execute(new ActionExecutionContext());
            }
            else Evaluate(true);
        }
    }
}

The interesting thing I did here was to reuse the IResult functionality for async shutdown of the application. Here’s how the custom strategy uses it:

  1. Check each IWorkspace to see if it is an IConductor.
  2. If true, grab all the conducted items which implement the application-specific interface IHaveShutdownTask.
  3. Retrieve the shutdown task by calling GetShutdownTask. It will return null if there is no task, so filter those out.
  4. Since the shutdown task is an IResult, pass all of these to a SequentialResult and begin enumeration.
  5. The IResult can set ResultCompletionEventArgs.WasCanceled to true to cancel the application shutdown.
  6. Continue through all workspaces until finished or cancellation occurs.
  7. If all IResults complete successfully, the application will be allowed to close.

The CustomerViewModel and OrderViewModel use this mechanism to display a modal dialog if there is dirty data. But, you could also use this for any number of async tasks. For example, suppose you had some long running process that you wanted to prevent shutdown of the application. This would work quite nicely for that too.

 

This concludes our short mini-series on Screens and Conductors. I hope I have provided enough theory, documentation and samples to get you going in the right direction. That said, if you don’t understand these concepts or don’t see the problems in your app that these were intended to solve, then don’t use them. Screens/Conductors are best used when you encounter the sorts of engineering difficulties that they were intended for. Simply inheriting willy-nilly from these base classes will likely add unnecessary complexity to your application. As always, use every feature intentionally to solve a specific problem…not just because it’s there Smile If in doubt, avoid Screens/Conductors. When the need arises, you’ll know what they are and where to use them.

 

Next Up: All About Conventions


Posted 11-18-2010 4:36 PM by Rob Eisenberg

[Advertisement]

Comments

Sean wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-19-2010 8:08 AM

Awesome stuff Rob. Thanks for the sample, this was a great help.

Mike wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-19-2010 5:20 PM

I agree with Sean this was an awesome post Rob!

Sant wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-20-2010 7:25 PM

First Great job, Rob.

There is an issue with closing a child view (Customer View). To reproduce

a) Create 2 customers.

b) Close the app, it pops up unsaved message for first customer, say yes, for 2nd customer, say no. App does not close but then in Customer work space it still shows both the customers. Should show only 1 (the unsaved customer)

Sant wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-20-2010 7:40 PM

Never mind about my earlier comment about closing issue. I assumed saving means Save + Close.

Rob Eisenberg wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-20-2010 10:26 PM

@Sant

That behavior could be changed if desired. You would do it by altering the implementation of ApplicationCloseStrategy.

Replacing MEF with IoC wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-22-2010 6:59 PM

Thanks Rob.

How do I proceed if I need to replace MEF with an IoC container in this demo, say by StructureMap.

Rob Eisenberg wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-22-2010 7:18 PM

I don't know if StructureMap works with Silverlight. So,, you would have to check on that. Assuming that it did, you would override the same methods in the Bootstrapper that the MEF version does, just implement each one to forward calls on to StructureMaps container. Here's a forums discussion with some sample code caliburnmicro.codeplex.com/.../View.aspx

pFaz wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-26-2010 4:13 AM

Hi Rob

quick question re blendability and design time data. In your mix talk you mentioned support for this coming in full Caliburn. is there anything in CM for this at the moment? Is the only option to add binding statements to your view to override the conventions?

Robert wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-28-2010 9:31 PM

Thanks for the nice demo, Rob!

I noticed there seems to be a slight bug in the animation when switching between customers which I'm not sure how/where to correct.

Here's the quickest way to see the animation artifact I'm talking about:

(1) Click Customers workspace button at bottom

(2) Click New button to add Customer 1

(3) Click Customers workspace button at bottom

(4) Click New button to add Customer 2

(6) Click EditAddress button to display modal dialog for Customer 2

(7) Click Customers workspace button at bottom

(8) Click + next to Customer 1 and watch the animation carefully...

Notice that the image that slides in from the left is of Customer 2's screen, which is easily seen because it's displaying the modal dialog. However, the correct screen for Customer 1 is shown when the animation finishes. It looks like the previously seen Customer detail is used for the animation instead of the one it's switching to.

Anyway, thanks again for all of the great work you're doing with Caliburn (and now Caliburn Micro)!

Robert

Robert wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-28-2010 9:33 PM

Thanks for the nice demo, Rob!

I noticed there seems to be a slight bug in the animation when switching between customers which I'm not sure how/where to correct.

Here's the quickest way to see the animation artifact I'm talking about:

(1) Click Customers workspace button at bottom

(2) Click New button to add Customer 1

(3) Click Customers workspace button at bottom

(4) Click New button to add Customer 2

(6) Click EditAddress button to display modal dialog for Customer 2

(7) Click Customers workspace button at bottom

(8) Click + next to Customer 1 and watch the animation carefully...

Notice that the image that slides in from the left is of Customer 2's screen, which is easily seen because it's displaying the modal dialog. However, the correct screen for Customer 1 is shown when the animation finishes. It looks like the previously seen Customer detail is used for the animation instead of the one it's switching to.

Anyway, thanks again for all of the great work you're doing with Caliburn (and now Caliburn Micro)!

Robert

(sorry if this is a duplicate post - I can't see my comment anywhere)

Robert wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 11-28-2010 10:25 PM

Actually, it looks like it might be sliding in the new Customer 1 screen correctly AND simultaneously sliding in and fading out the previous Customer 2 one?!? Is this the expected behavior?

I narrowed it down to the specialTransition style for the TransitioningContentControl in the NamedStyles.xaml file, but I don't understand what's happening well enough to change it to what I expect (eliminate the distracting Customer 2 fade-out that looks like a glitch to me). Any help would be appreciated!

Robert

Adrian wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 12-09-2010 4:10 AM

Hi Rob,

Great example you put here.

I wanted to copy the DialogConductor and MessageBox implementation in a project of mine using Ninject instead of MEF and it worked great using the DefaultCloseStrategy.

However, I noticed that in the ShowMessageBox method from the DialogConductorViewModel, you are subscribing to the MessageBox.Deactivated event, without unsubscribing. Is this the source for a memory leak or is MEF taking care of things in a way or another. (I haven't used MEF)

Thanks a lot for all the beautiful code that you write.

Adrian

Greg wrote re: Caliburn.Micro Soup to Nuts Part 6d – A “Billy Hollis” Hybrid Shell
on 12-18-2010 5:43 PM

Is there a WPF version of this sample available?  I've found myself spending many hours trying to get a WPF version up and running, but keep hitting road blocks.

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)