ISupportCustomShutdown

Prerequisites

  1. Obtain and Build the Code
  2. Setting Up a Project

Referenced Assemblies

  • Caliburn.Core
  • Caliburn.PresentationFramework
  • Microsoft.Practices.ServiceLocation

Explanation

When an IPresenterHost is asked to shutdown one of the IPresenters that it is hosting it will call IPresenter.CanShutdown(). If this returns true, the IPresenterHost will shutdown and everything progresses normally. If it returns false, things get interesting. At that point the default implementations of IPresenterHost will attempt to cast the IPresenter as ISupportCustomShutdown. If the cast is unsuccessful, the shutdown will be cancelled. If the cast is successful, the IPresenterHost will invoke the methods of the ISupportCustomShutdown in order to give the IPresenter a "second chance" to answer the shutdown request. Here's how it works:
  1. The IPresenterHost calls the ISupportCustomShutdown.CreateShutdownModel() to get a ViewModel representing a UI which should be shown to the user for additional feedback.
  2. The ShutdownModel is passed to the ExecuteShutdownModel on the IPresenterHost which should ultimately interpret the meaning of the model and display something to the user.
  3. When user feedback is received as an update to the ShutdownModel, the implementation of ExecuteShutdownModel should call the completion delegate.
  4. The IPresenterHost then passes the ShutdownModel back to the IPresenter for interpretation by calling the ISupportCustomShutdown.CanShutdown() method.
  5. If that method returns true, the IPresenter is shutdown, otherwise, shutdown is cancelled.

Implementing ISupportCustomShutdown

All the default implementations of IPresenterHost implement this interface. To take advantage of custom shutdown functionality in your own apps, you must do the following:
  1. Implement ISupportCustomShutdown on the IPresenters in your app.
  2. Override ExecuteShutdownModel on any IPresenterHost which you wish to be able to display custom shutdown UI for its children.

Below is an example of how you might choose to accomplish these two things.

First, create an implementation of ISubordinate which represents the custom UI and is returned by ISupportCustomShutdown.CreateShutdownModel() (A subordinate is a model owned by an IPresenter.) You might create something like this:
public class Question : PropertyChangedBase, ISubordinate
{
    private Answer _answer = Answer.No;

    public Question(IPresenter master, string text)
        : this(master, text, Answer.No, Answer.Yes, Answer.Cancel) {}

    public Question(IPresenter master, string text, params Answer[] possibleAnswers)
    {
        Master = master;
        Text = text;
        PossibleAnswers = new BindableEnumCollection<Answer>(possibleAnswers);
    }

    public IPresenter Master { get; private set; }
    public string Text { get; private set; }
    public BindableEnumCollection<Answer> PossibleAnswers { get; set; }

    public Answer Answer
    {
        get { return _answer; }
        set
        {
            _answer = value;
            NotifyOfPropertyChange("Answer");
        }
    }
}

Your IPresenter may return an instance of this like so:
public ISubordinate CreateShutdownModel()
{
    if(Contact.IsValid)
    {
        return new Question(
            this,
            string.Format(
                "Contact '{0}' has not been saved.  Do you want to save before closing?",
                (Contact.LastName ?? string.Empty) + ", " + (Contact.FirstName ?? string.Empty)
                )
            ) {Answer = Answer.Yes};
    }

    return new Question(
        this,
        string.Format(
            "Contact '{0}' is invalid.  Changes will be lost.  Do you still want to close?",
            (Contact.LastName ?? string.Empty) + ", " + (Contact.FirstName ?? string.Empty)
            ),
        Answer.Yes, Answer.No
        );
}

You must then override ExecuteShutdownModel in your IPresenterHost.
protected override void ExecuteShutdownModel(ISubordinate model, System.Action completed)
{
    var dialog = ServiceLocator.GetInstance<IQuestionDialog>();
    dialog.Setup("Warning", model.GetQuestions());
    dialog.WasShutdown += delegate { completed(); };
    ServiceLocator.GetInstance<IShell>().PushDialog(dialog);
}

public static IEnumerable<Question> GetQuestions(this ISubordinate model)
{
    var queue = new Queue<ISubordinate>();
    queue.Enqueue(model);

    while(queue.Count > 0)
    {
        var current = queue.Dequeue();
        var anotherComposite = current as ISubordinateComposite;

        if(anotherComposite != null)
            anotherCompoiste.GetChildren().Apply(queue.Enqueue);
        else yield return (Question)current;
    }
}

Note: The imlementation of ExecuteShutdownModel/GetQuestions takes into account the fact they we may be trying to shutdown a hierarchical model. This is because the IPresenter being shutdown may actually be another IPresenterHost. In order to handle this scenario, it flattens the Question hierarchy into a simple list of questions which it can use to display to the user.

Note: PresenterManager returns a SubordinateContainer from ISupportCustomShutdown.CreateShutdownModel() if its CurrentPresenter is an ISupportCustomShutdown. MultipPresenterManager returns a SubordinateGroup which contains the shutdown models of all its children which could not be shutdown by conventional means. The same is true of MultiPresenter.

For completeness, the implementation of IQuestionDialog might look like this:
public class QuestionDialogViewModel : Presenter, IQuestionDialog
{
    private string _caption;

    public QuestionDialogViewModel(IShell shell)
        : base(shell)
    {
    }

    public bool HasOneQuestion
    {
        get { return Questions.Count == 1; }
    }

    public bool HasMultipleQuestions
    {
        get { return Questions.Count > 1; }
    }

    public Question FirstQuestion
    {
        get { return Questions[0]; }
    }

    public string Caption
    {
        get { return _caption; }
        set { _caption = value; NotifyOfPropertyChange("Caption"); }
    }

    public BindableCollection<Question> Questions { get; private set; }

    public void Setup(string caption, IEnumerable<Question> questions)
    {
        Caption = caption;
        Questions = new BindableCollection<Question>(questions);
    }

    public void SelectAnswer(BindableEnum answer)
    {
        FirstQuestion.Answer = (Answer)answer.Value;
        Close();
    }
}

And the view might look like this:
<UserControl x:Class="SomeProject.Views.QuestionDialogView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:pf="clr-namespace:Caliburn.PresentationFramework;assembly=Caliburn.PresentationFramework"
             Width="400"
             MaxHeight="600">

    <Grid>
        <Grid x:Name="LayoutRoot"
              Visibility="{Binding HasOneQuestion, Converter={StaticResource booleanToVisibility}}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <TextBlock Text="{Binding Caption}" />

            <TextBlock Text="{Binding FirstQuestion.Text}"
                       Grid.Row="1"
                       TextWrapping="Wrap" />

            <ItemsControl HorizontalAlignment="Right"
                          ItemsSource="{Binding FirstQuestion.PossibleAnswers}"
                          Grid.Row="2">
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel Orientation="Horizontal" />
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Button Content="{Binding}"
                                pf:Message.Attach="SelectAnswer($value)" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
            </ItemsControl>
        </Grid>

        <Grid Visibility="{Binding HasMultipleQuestions, Converter={StaticResource booleanToVisibility}}">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>

            <TextBlock Text="Warning!"/>

            <ScrollViewer BorderThickness="0"
                          VerticalScrollBarVisibility="Auto"
                          Grid.Row="1">
                <ItemsControl ItemsSource="{Binding Questions}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Border>
                                <StackPanel>
                                    <TextBlock Text="{Binding Text}" />
                                    <ComboBox HorizontalAlignment="Right"
                                              ItemsSource="{Binding PossibleAnswers}"
                                              SelectedItem="{Binding Answer, Converter={StaticResource enumConverter}, Mode=TwoWay}" />
                                </StackPanel>
                            </Border>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </ScrollViewer>

            <Button Content="Done"
                    HorizontalAlignment="Right"
                    Grid.Row="2"
                    pf:Message.Attach="Close" />
        </Grid>
    </Grid>
</UserControl>

Note: It is important to note that the implementation of ISubordinate and the interpretation done by ExecuteShutdownModel are application specific. However, you may find the samples shown above suitable for a number of common situations.

Last edited Oct 23, 2009 at 6:52 PM by EisenbergEffect, version 12