Coproject - a RIA Caliburn.Micro demo, part 8

by Augustin Šulc

In this part, we will create ToDoItem detail.

Context lifetime

The main question when considering lists and details is whether to share context among all screens or create a new context per screen. Both ways have their pros and cons and you will probably have to choose depending on the situation. But most likely, you will use both of them depending on the scope of entities.

Shared context

In this case, you would probably create a service in the application and all screens would access it to get and save data. It is useful when some entities are used across the whole application.

+ You can pass entities across application
+ No need to synchronize data when an entity gets updated – all parts of the application use the same entity.
+ Lower memory consumption if entities are shared across application.
+ Centralized data access.
- Entities stay in memory until detached or context disposed. So if you use a shared context to load hundreds of entites (e.g, in a grid) and do not detach them, memory consumption will be a problem.
- Even unsaved state of entities affect other parts of application (sharing the same entity).
- Harder saving. If you save whole context, you might save currently edited entities of other screens.
- Harder to maintain, especially with current memory leaks in Silverlight.

Context per screen

Every screen creates its own context and loads and saves data on its own. This is probably better for scenarios with editing detail screens and large lists/grids.

+ Easier to see consequences of changing entity properties.
+ Memory management. You can easily dispose of context (and thus all entities it loaded).
+ Easier saving. If you save context, you don’t save currently edited entities of other screens.
- Need to pass IDs across application and every screen must load the entity itself.
- Duplicate entities might exist in the application.
- Need to synchronize changes throughout application when entity is updated.
- Possible concurrency errors while saving.

ToDoItemViewModel

In Coproject, we will use Context per screen for ToDoItem entity. So, let’s start. I hope you have done all the steps described so far. If you haven’t, just download source code from Coproject Codeplex site. Note that source codes are branched for every tutorial part so you can start in any stage.

First of all, we should create an interface for ToDoItem detail. So, create interface ViewModels.Interfaces.IToDoItemEditor:

public interface IToDoItemEditor
{
	ToDoItem Item { get; }
	IEnumerable<IResult> Setup(int toDoItemID);
}

Then, add ToDoItemViewModel to ViewModels:

[Export(typeof(IToDoItemEditor))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ToDoItemViewModel : Screen, IToDoItemEditor
{
	public ToDoItem Item { get; private set; }

	private CoprojectContext _context;

	public IEnumerable<IResult> Setup(int toDoItemID)
	{
			_context = new CoprojectContext();

			var dataResult = new LoadDataResult<ToDoItem>(
				_context,
				_context.GetToDoItemsQuery().Where(x => x.ToDoItemID == toDoItemID));
			yield return dataResult;

			Item = dataResult.Result.Entities.First();
			NotifyOfPropertyChange(() => Item);
	}
}

As you can see, the detail screen expects ToDoItemID and loads the proper entity itself. We want to store the context because we will use it for saving. Since there will be single instance of view model for every ToDoItem detail opened, we need it to be of a NonShared creation policy.

Now, let’s add the logic that will open the detail screen. There are ListBoxes in DataTeplates for every ToDoList in ToDoListsView so we could probably use the same trick as for modules to track the selected item, but as there are more ListBoxes, sharing SelectedItem would lead at least to a binding error.  Moreover, I want to show how to use custom binding to events.

Firstly, open ToDoListsViewModel and change its base class from Screen to

Conductor<IToDoItemEditor>.Collection.OneActive

and add the following function to it:

public IEnumerable<IResult> OpenItemDetail(ToDoItem item)
{
	var editor = Items.FirstOrDefault(x => x.Item.ToDoItemID == item.ToDoItemID);
	if (editor == null)
	{
		editor = IoC.Get<IToDoItemEditor>();
		yield return editor.Setup(item.ToDoItemID).ToSequential();
	}

	ActivateItem(editor);
	yield break;
}

Finally, we need to execute this function in appropriate time. Open ToDoListsView and add new xmlns definition to the top of the file:

xmlns:cal="http://www.caliburnproject.org"

Now, we want to attach the OpenItemDetail function to the SelectionChanged event of the ListBox bound to ToDoItems. It the most fluent way, it would be done like this:

<ListBox ItemsSource="{Binding ToDoItems}" 
			cal:Message.Attach="[Event SelectionChanged] = [Action OpenItemDetail($this.SelectedItem)]" ...

But since SelectionChanged is the default event of ListBox, Action is the default behavior and SelectedItem is the default property of ListBox for binding, we can use this shortcut with the same result:

<ListBox ItemsSource="{Binding ToDoItems}" cal:Message.Attach="OpenItemDetail($this)"

Note the argument $this. This is a special C.M argument name. Other special words are $datacontext and $eventargs. Their meaning is exactly what you would expect from their names. You can read more about C.M actions here.

We also must add a place for the detail to be displayed in. So add this control below the ScrollViewer containing the Lists ItemsControl:

<ContentControl x:Name="ActiveItem" Grid.Column="1" Grid.Row="1" Margin="10,0,0,0" 
				HorizontalContentAlignment="Stretch" />

ToDoItemView

Next, we have to create the respective view. So add a new Silvelright User Control named ToDoItemView to Views. Add these two namespaces to it:

xmlns:dataForm="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm.Toolkit"
xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"

Then add this DataForm to LayoutGrid:

<dataForm:DataForm CurrentItem="{Binding Item}" HorizontalAlignment="Stretch">
	<StackPanel>
		<dataForm:DataField PropertyPath="DueDate">
			<controls:DatePicker SelectedDate="{Binding DueDate,Mode=TwoWay}" />
		</dataForm:DataField>
		<dataForm:DataField PropertyPath="Content">
			<TextBox Text="{Binding Content,Mode=TwoWay}" TextWrapping="Wrap" Height="auto" />
		</dataForm:DataField>
		<dataForm:DataField PropertyPath="User">
			<TextBlock Text="{Binding User.LastName}" />
		</dataForm:DataField>
	</StackPanel>
</dataForm:DataForm>

If you run the application, you will see this:
image

DataFields automatically added labels to proper places. You should note that there are no DataForm buttons. The reason is that in Assets/Custom.xaml, we have a default style that disables it. DataForm is still kind of a buggy control so I want to use as little of its features as possible.

Let me mention four DataForm bugs we could encounter in Coproject:

  • Even though Commit and Cancel buttons are disabled, they sometimes appear when changing edit mode.
  • When switching between DataForms, it sometimes hide the first label in it (DueDate).
  • When setting ReadOnly mode after editing a TextBox inside the DataForm, controls inside the DataForm are still enabled.
  • Switching to edit mode with BusyIndicator on the same page causes the indicator to constantly use 100% CPU.

All of them, except for the second can be avoided / fixed.

You probably noticed that User is still empty. The cause is that this nested entity is not included in ToDoItem. So open Services/CoProjectService.cs in the server project, find GetToDoItems() and edit it as follows:

return this.ObjectContext.ToDoItems.Include("User");

Build and run the application. Detail should be complete:
image

Tags: Silverlight, Caliburn, Ria, Coproject

7 Comments

  • vmlf said

    Hello, insignificant detail but in the OpenItemDetail method, why is there a "yield break;" at the end? Can't come up with a reason for it. Thanks

  • Augustin Šulc said

    If editor variable is not null, no result would be returned so we have to yield break. I guess it would not even build without it.

  • Leo said

    Hi,

    I modified your solution to switch to DbDomainService and use EF 4.1. I have a problem now with selection changed event fired by Caliburn Micro. At first, the items are called but once I switch an item and go back to the previous item that was loaded, then the SelectionChanged Action won't be triggered. The OpenItemDetails method does not fire again. I don't know why. It is kind of odd though.I am also using Autofac instead of MEF.

    I don't see the reason why, I tried making a simple listbox inside it instead of binding to ToDoItems. The method is properly working fine, even if I switch back and forth. But if I binded it to the ToDoItems its no longer being called again. Just once.

    Note: I know your solution works cause I tried everything from part 1 to part 14. Although I don't see the main reason why what would cause this behavior.

    Regards,

  • Augustin Šulc said

    Hi,
    I'd try to debug the application and see if there aren't any exceptions in the C.M stack. Usually, if nothing happens when an action should be called, it is because of C.M silently failing with an exception.
    Otherwise, from your description, I am not able to guess where the problem is :-( Also, don't you have problems with component lifetime in Autofac? The original solution uses NonShared CreationPolicy so a new instance is created everytime MEF is asked for a component (ie. no singleton).

  • Leo said

    Hi Augustin,

    What would be the problems using Autofac's component lifetime? I registered it as a not single instance so every time I yield return it is a new instance of that viewmodel. I inject it with a Func and delegate it on the Action. I converted everything from MEF to Autofac and then Linq2Entities to DbDomainContext. I don't think I am having problems with the Service refactored one because it is working fine doing CRUD operations in the database from the client side. But with the CM, I don't know what happened I also tried to debug but it does't give me a silent fail.

    It performs the SelectionChanged in the ListBox for the first time as long as the items are not yet loaded in the Items. Once it is loaded it won't call the SelectionChanged action again.

    e.g. I have 2 items in the ListBox and I click on one of the items it loads, second, it load, go back to the first time it doesn't fire the action anymore. Have you encountered issues like this?

  • Augustin Šulc said

    I thought you might have Autofac configured wrong but as you say, it seems to be ok.
    I haven't had similar issues. I can only think of debuging using C.M source code to excatly see what happens (and what does not).

  • Leo said

    Hi Augustin,

    I figured it out. It seems that the behavior of the ListBox SelectedItem with two different listbox as a children doesn't unselect the item so it stays as the same. Had a work around for it. No issues with autofac, C.M., Entity, etc.

Add a Comment