Coproject - a RIA Caliburn.Micro demo, part 11

by Augustin Šulc

In this part, we will synchronize list after saving data and add some icons. You may continue on your work from older parts, or download updated code from Coproject site.

Event aggregator

Usually, if you want a component of your application to notify other components about an event that occurred, you use standard .NET events. The problem with events is that you always need to access the publisher of an event from the subscriber. What’s more, you also need to unsubscribe from the event. That might be tricky in a complicated application or when the same type of event may come from more sources (for example if you would implement user events logging via events and logger subscribed to these events).

Here comes (again) Caliburn.Micro and its implementation of Event Aggregator. Consider EventAggregator as a component that you subscribe to all kinds of events or that you use to publish an event.

In Coproject, we will use EventAggregator to notify ToDoItemsViewModel that ToDoItem has been changed and thus it should reload data.

First of all, we must create an object that will represent the event. In Coproject client project, create new folder Events and put a new class called ToDoItemUpdatedEvent into it:

public class ToDoItemUpdatedEvent
{
	public int ToDoItemID { get; set; }
	public ToDoItemUpdatedEvent(int toDoItemID)
	{
		ToDoItemID = toDoItemID;
	}
}

Open ToDoItemViewModel and let MEF import an EventAggregator instance:

[Import]
public IEventAggregator EventAggregator { get; set; }

Then update Save as follows:

public IEnumerable<IResult> Save()
{
	(Item as IEditableObject).EndEdit();
	IsReadOnly = true;
	#region Fix DataForm bug
	IsReadOnly = false; IsReadOnly = true;
	#endregion

	yield return new SaveDataResult(_context);

	EventAggregator.Publish(new ToDoItemUpdatedEvent(Item.ToDoItemID));
}

So, after (remember coroutines and yield return) a ToDoItem is saved, we publish event notifying about it. We do not care who subscribes to this event, this is not our business here.

Now, let’s write the other end – an event subscriber. Open ToDoListsViewModel and edit the constructor:

[ImportingConstructor]
public ToDoListsViewModel(IEventAggregator eventAggregator)
{
	DisplayName = "To Do";
	Description = "To-do lists";

	eventAggregator.Subscribe(this);
}

Now, event aggregator will know that it should notify ToDoListsViewModel about events. But what events? It depends on what IHandle<T> interfaces the subscriber implements. Let the view model implement IHandle<ToDoItemUpdatedEvent> and add this implementation:

public void Handle(ToDoItemUpdatedEvent message)
{
	LoadData().ToSequential().Execute(null);
}

Remember that LoadData returns iEnumerable<IResult> so we have to enumerate it to ‘run’ it. Therefore, we wrap it into a SequentialResult and then execute this single result.

The only problem is that LoadData requires a parameter – filter. And this value is unknown to the handler. Fortunately, we can change the source Filter textbox binding from function parameter to property. Add this property to ToDoListsViewMode:

public string Filter { get; set; }

And the remove parameter ‘filter’ from LoadData (take the filter value from Filter).

Build the application – it should run as before, but after saving of a ToDoItem, the list should refresh automatically.

Icons

Let’s give Coproject a little more style – change text buttons to icon buttons.

Open the zip file attached to this post and extract the five images to Assets/Icons. These icons are from Axialis.

Then, open ToDoListsView and edit the LoadData button:

<Button x:Name="LoadData" Grid.Column="1" ToolTipService.ToolTip="Search">
	<Button.Content>
		<Image Source="/Coproject;component/Assets/Icons/Search.png" Height="20" />
	</Button.Content>
</Button>

Well, this works nicely but let’s make it a little more shorter since we will probably use these image buttons quite frequently in our application. To do this, we will create a custom control called ImageButton. Create a new folder Controls and add this control to it:

public class ImageButton : Button
{
	public string ImageName
	{
		get { return (string)GetValue(ImageNameProperty); }
		set { SetValue(ImageNameProperty, value); }
	}

	public static readonly DependencyProperty ImageNameProperty =
		DependencyProperty.Register("ImageName", typeof(string), typeof(ImageButton), new PropertyMetadata(OnImageNamePropertyChanged));

	public static void OnImageNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
	{
		ImageButton button = d as ImageButton;
		string newValue = e.NewValue as string;

		if (newValue.IsNullOrWhiteSpace())
		{
			button.Content = null;
			return;
		}

		newValue = "/Coproject;component/Assets/Icons/{0}.png".FormatWith(newValue);
		ImageSource image = new BitmapImage(new Uri(newValue, UriKind.Relative));
		button.Content = new Image { Source = image };
	}
}

As you can see, we inherited original Button and added a property to it. The reason that ImageName is implemented as a dependency property is to enable it for binding, styling, etc. When the property is changed a new Image is inserted to the button. It would be great to add this image into ContentTemplate by style and just bind its Source property to ImageName, but since in template binding, you cannot use any converters, we would have to set ImageName in the raw form (/Coproject;component/Assets/Icons/Search.png) and that is not what we want. So that is why I’ve chosen this way. Now open Assets/Cosmopolitan/Custom.xaml and add this namespace to it:

xmlns:local="clr-namespace:Coproject.Controls"

And then add this to the end of the file, just above </ResourceDictionary>:

<Style TargetType="local:ImageButton" BasedOn="{StaticResource DefaultButtonStyle}">
	<Setter Property="Height" Value="32" />
</Style>

We are done – get back to ToDoListsView, register namespace for ImageButton as in Custom.xaml and update LoadData button:

<local:ImageButton x:Name="LoadData" Grid.Column="1" ImageName="Search" ToolTipService.ToolTip="Search" />

Update buttons in Toolbar view in the same manner:

<local:ImageButton x:Name="Edit" ImageName="Pen" ToolTipService.ToolTip="Edit" Margin="0,0,5,0" />
<local:ImageButton x:Name="Cancel" ImageName="Undo" ToolTipService.ToolTip="Cancel" Margin="0,0,5,0" />
<local:ImageButton x:Name="Save" ImageName="Save" ToolTipService.ToolTip="Save" Margin="0,0,5,0" />
<local:ImageButton x:Name="TryClose" ImageName="Close" ToolTipService.ToolTip="Close" />

That is all. I hope you like the new look!

Tags: Silverlight, Caliburn, Ria, Coproject

2 Comments

  • Albert van Peppen said

    When I found myself on this page of the tutorial I found that the eventhandling didn't work as descruibed, but gave it no notion since I wanted to run through the entire tutorial first.

    After doing the entire tutorial I came back to this point to figure out what it was that I was doing wrong.

    I found that you don't describe that you should derive the ToDoListsViewModel from IHandle<ToDoItemUpdatedEvent> as well; that was the trigger why it didn't work.

    Is this correct or did I missed something else?

    My class definition now looks like: (Note that I already did the rest of the tutorial)

    public class ToDoListsViewModel : Conductor<IToDoItemEditor>.Collection.OneActive, IModule, IHandle<ToDoItemUpdatedEvent>

    Instead of:

    public class ToDoListsViewModel : Conductor<IToDoItemEditor>.Collection.OneActive, IModule

    Albert

  • Augustin Šulc said

    @Albert
    It is mentioned in the last sentence before code exaple with "public void Handle..." - the sentence starting with "Let the view model implement IHandle<ToDoItemUpdatedEvent>..."

    I'll make it bold to be more emphasised.

Add a Comment