Coproject - a RIA Caliburn.Micro demo, part 9

by Augustin Šulc

In this part, we will add a toolbar and handle data saving.

Toolbar

It is a typical scenario that screens have toolbars. These are usually shown somewhere else in the application than their respective view model’s views. In this case, we will benefit from Caliburn.Micro’s feature of having more than one view attached to a view model. You can describe what view you want to show by setting ‘context’ of the view.

So, open the Coproject solution where we finished the last time or download source codes from Coproject site. Add this control to root grid in ToDoListsView:

<ContentControl Grid.Column="1" Grid.Row="0" Margin="10,0,0,0" HorizontalContentAlignment="Stretch" 
				cal:View.Model="{Binding ActiveItem}" cal:View.Context="Toolbar" />

Important is the second line. Since there is already a control called ActiveItem and we want to bind this control to the active item too, we have to use cal:View.Model structure. Even more important is cal:View.Context because it instructs C.M not to use the default view (ToDoItemView in this case) but Toolbar view.

C.M’s default logic for locating context views is: take the default view location (e.g., Views.ToDoItemView), trim ‘View’ from the end, add ‘.’ + context. So it would end up with Views.ToDoItem.Toolbar. And that’s where we have to put our new Silverlight User Control. Then replace its original LayoutRoot with the following:

<Border Style="{StaticResource FilterPanelStyle}">
	<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
		<Button x:Name="Edit" Content="Edit" Margin="0,0,5,0" />
		<Button x:Name="Cancel" Content="Cancel" Margin="0,0,5,0" />
		<Button x:Name="Save" Content="Save" Margin="0,0,5,0" />
		<Button x:Name="TryClose" Content="Close" />
	</StackPanel>
</Border>

So the structure of the client project should be like:
image

Context views are bound to view models the same way as default views, so it is not a surprise that now we are going to implement Edit, Cancel, and Save functions (TryClose is already implemented in Screen base class).

ViewModel behavior

We want the detail to be in read-only mode with buttons Cancel and Save disabled. When Edit is clicked, the detail should change to edit mode and disable edit button. This will be done by adding another property IsReadOnly to the view model. So, open ToDoItemViewModel.

First of all, we will add the IsReadOnly property. Since we want it to automatically raise NotifyPropertyChanged, we cannot use the one-line definition, so write this:

private bool _isReadOnly;
public bool IsReadOnly
{
	get
	{
		return this._isReadOnly;
	}
	set
	{
		this._isReadOnly = value;
		NotifyOfPropertyChange(() => IsReadOnly);
	}
}

Implementation of Edit function is very lightweight:

public void Edit()
{
	IsReadOnly = false;
}

Cancel will be only a little bit longer because we need to cancel changes before switching back to read-only mode. We will benefit from the fact that RIA Services entities implement IEditableObject interface. In order to fix a DataForm bug I mention last time, we have to switch to read-only, then back to edit, and finally again to read-only mode:

public void Cancel()
{
	(Item as IEditableObject).CancelEdit();
	IsReadOnly = true;
	#region Fix DataForm bug
	IsReadOnly = false; IsReadOnly = true;
	#endregion
}

In order to implement Save(), we will need SaveDataResult. It is wuite similar to LoadDataResult we implemented earlier. So create new class SaveDataResult into the Framework folder:

namespace Coproject.Framework
{
	public class SaveDataResult : IResult
	{
		public DomainContext Context { get; private set; }
		public SubmitOperation Result { get; private set; }

		public event EventHandler<ResultCompletionEventArgs> Completed;

		public SaveDataResult(DomainContext context)
		{
			Context = context;
		}

		public void Execute(ActionExecutionContext context)
		{
			Context.SubmitChanges(SaveDataCallback, null);
		}

		private void SaveDataCallback(SubmitOperation data)
		{
			Result = data;
			OnCompleted();
		}

		private void OnCompleted()
		{
			var handler = Completed;
			if (handler != null)
			{
				handler(this, new ResultCompletionEventArgs());
			}
		}
	}
}

Now, we can use it ToDoItemViewModel.Save() implementation:

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

	yield return new SaveDataResult(_context);
}

Finally, bind the DataForm on ToDoItemView to the IsReadOnly property:

<dataForm:DataForm CurrentItem="{Binding Item}" IsReadOnly="{Binding IsReadOnly}" HorizontalAlignment="Stretch">

If you now run the application, you will notice that switching modes works and savings works too (you must refresh the list). There still some issues: opened detail is already in edit mode, all buttons are enabled.

Fixing the first issue is trivial – add this row to the beginning of Setup(int toDoItemID):

IsReadOnly = true;

Guard methods

To fix the other issue, we will use ‘guards’. These are another part of Caliburn.Micro’s ‘Convention over configuration’ system. If there is a control bound to any function of a view model and the view model also contains property or function called CanXXX, it will use this CanXXX function to determine whether it is possible to run the bound function itself. These 'CanXXX’ properties are called guards. Note that C.M will also update availability of invoking controls. So as you can guess, we will implement properties CanEdit, CanCancel, and CanSave:

public bool CanEdit
{
	get { return IsReadOnly; }
}
		
public bool CanCancel
{
	get { return !IsReadOnly; }
}

public bool CanSave
{
	get { return Item.HasChanges && !Item.HasValidationErrors; }
}

We also need to notify when these properties change. So update set of IsReadOnly as follows:

set
{
	this._isReadOnly = value;
	NotifyOfPropertyChange(() => IsReadOnly);
	NotifyOfPropertyChange(() => CanEdit);
	NotifyOfPropertyChange(() => CanCancel);
}

CanSave property depends on the state of Item, so add this line at the end of the Setup function:

Item.PropertyChanged += (s, e) => NotifyOfPropertyChange(() => CanSave);

You can now run the application and see how the buttons get disabled and enabled as appropriate.

Dirty checking

As Close button works too, we should add dirty-checking logic so that we don’t lose unsaved changes when closing detail. This is done using CanClose virtual function. This function is closed when C.M wants to close the screen. It is then up to the implementation what happens but when it is resolved, the CanClose function should call callback with information whether it can be closed or not. Add this function to ToDoItemViewModel:

public override void CanClose(Action<bool> callback)
{
	if (Item.HasChanges)
	{
		var result = MessageBox.Show("Current item was not saved. Do you really want to close it?", 
			"Coproject", MessageBoxButton.OKCancel);
		callback(result == MessageBoxResult.OK);
	}
	else
	{
		callback(true);
	}
}

I know that the question in the message is not ok for buttons OK or Cancel but that’s all we get from Silverlight out of the box. You definitely should implement custom message box (I will talk about it in later posts).

There is one more thing I want to implement: if the detail is closed and its entity is edited, it would be better to cancel the changes. Implementation is very straightforward:

protected override void OnDeactivate(bool isClosing)
{
	if (isClosing && CanCancel)
	{
		Cancel();
	}
}

That is all for toolbar and dirty-checking. It was quite easy, wasn’t it?

UpdateSourceOnChange

I bet you noticed that when editing Content, you have to click somewhere else to enable the Save button. The reason for that is that TextBox in Silverlight updates its binding source only when it loses focus and this cannot be changed as in WPF by setting UpdateSourceTrigger=PropertyChanged. Fortunately, this can be achieved in Silverlight too, but we will need this helper class in Coproject/Helpers:

public static class BindingHelper
{
	public static bool GetUpdateSourceOnChange(DependencyObject obj)
	{
		return (bool)obj.GetValue(UpdateSourceOnChangeProperty);
	}

	public static void SetUpdateSourceOnChange(DependencyObject obj, bool value)
	{
		obj.SetValue(UpdateSourceOnChangeProperty, value);
	}

	public static readonly DependencyProperty UpdateSourceOnChangeProperty =
		DependencyProperty.RegisterAttached("UpdateSourceOnChange", typeof(bool), 
		typeof(BindingHelper), new PropertyMetadata(false, OnPropertyChanged));

	private static void OnPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
	{
		var textBox = obj as TextBox;
		if (textBox != null)
		{
			if ((bool)e.NewValue)
			{
				textBox.TextChanged += TextBox_TextChanged;
			}
			else
			{
				textBox.TextChanged -= TextBox_TextChanged;
			}
		}
	}

	private static void TextBox_TextChanged(object sender, TextChangedEventArgs e)
	{
		var textBox = sender as TextBox;
		if (textBox != null)
		{
			var binding = textBox.GetBindingExpression(TextBox.TextProperty);
			if (binding != null)
			{
				binding.UpdateSource();
			}
		}
	}
}

Your Solution Explorer should show this:
image

Lastly, we need to use this helper in our views. Open ToDoItemView and register this namespace in it:

xmlns:helpers="clr-namespace:Coproject.Helpers"

Then update the TextBox in DataField bound to Content as follows:

<TextBox Text="{Binding Content,Mode=TwoWay}" TextWrapping="Wrap" Height="auto"
			helpers:BindingHelper.UpdateSourceOnChange="True"/>

And that is really all for this part. Run the application and experiment!
image

Tags: Silverlight, Caliburn, Ria, Coproject

Add a Comment