Caliburn.Micro #006 : Event Aggregators & Window Managers

Consider the classic scenario when you need to show the currently logged in User Name in your main Window, once you have successfully logged in. The The login Window is supposed to be a Modal Dialog, and isn’t remotely aware of the Label displaying Username in the Main Window.

WPF handles Modal Dialogs and messaging service between Views using Window Managers and Event Aggregators.

Event Aggregators allows a loosely coupled message parsing mechanism between different View Models in the system. As the Caliburn Micro Documentation states, an Event Aggregator is a service that provides the ability to publish an object from one entity to another in a loosely based fashion.

Let’s being by creating our Sample Application using the Caliburn.Micro Template Pack, which makes it easier to kick start a Caliburn.Micro based application. The complete sample code described in this post is available in Github.
Capture

 

We would be using MEF as our IoC Container. So let’s make changes to Bootstrapper to accommodate MEF. For sake of ease, we will remove the existing Bootstrapper and add MEF Template supplied along with the Caliburn.Micro Template Pack.

 

Capture1
We will now our Contract Interface and the required LoginView as well as our MainWindow (we will it ShellView). The skeleton of code looks like following now.
ShellView.Xaml
 <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="50"></RowDefinition>
        </Grid.RowDefinitions>
        
        <StackPanel Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Label x:Name="UserName" FontWeight="Bold" FontSize="32"></Label>
        </StackPanel>
        
        <StackPanel Grid.Row="1" Margin="10,10,10,10">
            <Button Margin="5,5,5,5" cal:Message.Attach="[Event Click]=[Action PromptForLogin]">Login</Button>
        </StackPanel>
    </Grid> 

IShell Interface

    public interface IShell
    {

    }

ShellViewModel

  [Export(typeof(IShell))]
    public class ShellViewModel : PropertyChangedBase, IShell
    {
        private string _userName  = default;
        public string UserName
        {
            get => _userName;
            set
            {
                _userName = value;
                NotifyOfPropertyChange(nameof(UserName));
            }
        }

        public void PromptForLogin() {};

    }
We will add the skeleton code for Login Window as well before adding the code to invoke it as a separate Window (not Content Control).
LoginView.Xaml
 <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="20"></RowDefinition>
            <RowDefinition Height="30"></RowDefinition>
            <RowDefinition Height="30"></RowDefinition>
            <RowDefinition Height="30"></RowDefinition>
            <RowDefinition Height="20"></RowDefinition>
        </Grid.RowDefinitions>
        
        <StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Center" Background="Lavender">
            <Label>User Login</Label>
        </StackPanel>
        <TextBox Margin="5,5,5,5" Grid.Row="1" Name="UserName">UserName</TextBox>
        <TextBox Margin="5,5,5,5" Grid.Row="2" Name="Password">Password</TextBox>
        <Button Grid.Row="3" Margin="5,5,5,5" cal:Message.Attach="[Event Click]=[Action Validate(UserName,Password)]">Login</Button>
    </Grid>

ILogin Interface

public interface ILogin
{
    bool Validate(string userName, string passWord);
}

LoginViewModel

[Export(typeof(ILogin))]
public class LoginViewModel : ILogin
{
    // Since this is a demo, we will always return true
    public bool Validate(string userName, string passWord) => true;

}

Alright, so we have our barebone code ready. Time to link up the chain. The first task is to ensure we have ShellView as our primary start up Window, which we set by altering the code in our Bootstrapper.

protected override void OnStartup(object sender, StartupEventArgs e)
{
    DisplayRootViewFor();
}

The next task is to ensure the Login Window User Clicks Login Button. We do this by utilizing the Window Manager, which is injected (via Dependency Injection) into ShellViewModel. We will add a constructor for ShellViewModel decorated with the ImportingConstructor Attribute. We will also inject LoginViewModel as well in to the class. We will then use the WindowManager for invoking the LoginViewModel as a Modal Dialog. Our constructor and PromptForLogin Method looks like following now.

[ImportingConstructor]
public ShellViewModel(IWindowManager windowManager,ILogin loginWindow)
{

}

public void PromptForLogin() => _windowManager.ShowDialog(_loginWindow);

This opens up the Login Window for you. All good till now, the Login Window is displayed, and you type in the UserName & Password, and Click the Login button to invoke the Validate Method. But how do you pass the UserName information to the ShellViewModel ? This is where Event Aggregator comes into place.

Event Aggregators allows ViewModels to pass (broadcast) information (in a model) to any other View Models who has subscribed to the event. We will begin by creating the model which would contain the information that will passed along Event Aggregators.

public class UserInfoModel
{
     public string UserName { get; set; }
}

We will then inject an instance of EventAggregator in the LoginViewModel. We will also modify our Validate Method a bit. The LoginViewModel class now looks like following.

[Export(typeof(ILogin))]
public class LoginViewModel : ILogin
{
private IEventAggregator _eventAggregator;

[ImportingConstructor]
public LoginViewModel(IEventAggregator eventAggregator)
{
    _eventAggregator = eventAggregator;
    _eventAggregator.Subscribe(this);
}

// Since this is a demo, we will always return true
public bool Validate(string userName, string passWord)
{
    _eventAggregator.PublishOnUIThread(new UserInfoModel(){UserName = userName});
    return true;
}
}

As you can see, we have used the PublishOnUIThread Method of Event Aggregator to publish the UserInfoModel object to anyone listening. We will now move to our ShellViewModel and ensure the class is listening. We would also see how we access the message send by the Login View Model.

Let’s begin by modifying the ShellViewModel to inject the EventAggregator to ShellViewModel, Our constructor now looks as following.

[ImportingConstructor]
public ShellViewModel(IWindowManager windowManager,ILogin loginWindow, IEventAggregator eventAggregator)
{
    eventAggregator.Subscribe(this);
    windowManager.ShowDialog(loginWindow);
}

Now comes the most important part, we need to implement the IHandle interface, where T is the model of the event object, which in our case is UserInfoModel. The interface contains a single method Handle, which we implement soon. Prior to that, this is how our class declaration looks now.

public class ShellViewModel : PropertyChangedBase, IShell, IHandle

The Handle Method of IHandle Interface looks like following.

public void Handle(UserInfoModel message)
{
this.UserName = message.UserName;
}

As you can see, we have assigned the UserName property of ViewModel from the message passed by the EventAggregator. That’s all we need to do. The EventAggregator has passed the message to all subscribers in the message bus for that particular event model. We then capture the message by implementing the Handle Method in IHandle interface.

Advertisement

One thought on “Caliburn.Micro #006 : Event Aggregators & Window Managers

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s