Coproject - a RIA Caliburn.Micro demo, part 6

by Augustin Šulc

In this part, we will load data from database and start using Caliburn.Micro coroutines. Remember that you can get latest news and source code from Coproject Codeplex site.

ToDoListsView

Create a new Silverlight User Control called ToDoListsView in the Views folder. Then replace the original LayoutRoot grid with this one:

<Grid x:Name="LayoutRoot">
	<Grid.ColumnDefinitions>
		<ColumnDefinition Width="6*" />
		<ColumnDefinition Width="5*" />
	</Grid.ColumnDefinitions>
	<Grid.RowDefinitions>
		<RowDefinition Height="auto" />
		<RowDefinition Height="*" />
	</Grid.RowDefinitions>

	<Button x:Name="LoadData" Content="Load" />

	<ItemsControl x:Name="Lists" Grid.Row="1">
		<ItemsControl.ItemTemplate>
			<DataTemplate>
				<StackPanel>
					<TextBlock Text="{Binding Name}" FontWeight="Bold" 
								Style="{StaticResource DefaultTextBlockStyle}" />
					<TextBlock Text="{Binding Description}" TextWrapping="Wrap" 
								Style="{StaticResource DefaultTextBlockStyle}" />
				</StackPanel>
			</DataTemplate>
		</ItemsControl.ItemTemplate>
	</ItemsControl>
</Grid>

As we already know, Caliburn.Micro will automatically bind Lists ItemsControl to ‘Lists’ property of respective view model. Therefore, you can guess that pressing LoadData button will execute function called LoadData, and you would be right. So let’s implement the view model.

ToDoListsViewModel

Open ToDoListsViewModel and add this code to it:

public IEnumerable<ToDoList> Lists { get; private set; }
public void LoadData()
{
	CoprojectContext context = new CoprojectContext();
	EntityQuery<ToDoList> query = context.GetToDoListsQuery().OrderByDescending(l => l.CreatedDate);
	context.Load(query, LoadDataCallback, null);
}

private void LoadDataCallback(LoadOperation<ToDoList> data)
{
	Lists = data.Entities;
	NotifyOfPropertyChange(() => Lists);
}

 

In Silverlight, all data loading is done asynchronously. It is good for keeping user interface responsive but programming asynchronous tasks requires more coding than working synchronously. To load data from a RIA Services service, you need to instantiate a context (created on compile time by RIA services), get default query from it, modify the query and then pass it back to the context to load it. You should also provide a callback that gets called when the data are loaded.

If you build the project and click Load, after a while, data should appear.
image

Coroutines

You probably noticed that if we wanted to do some other work after data are loaded, we would have to put it into LoadDataCallback. And if we wanted to load some other data, another callback would have to be created. This could easily become a a nightmare to maintain and debug. Happily, Caliburn.Micro comes to help with coroutines.

Caliburn coroutines are based on objects that implement IResult interface. Purpose of these objects is to execute some action and fire an event when the action is done. C.M then sequentially enumerates results returned from methods like LoadData, executes a result, waits for it to complete and then execute next one. This is achieved by yield return C# keyword. You probably see similarity to new keywords ‘async’ and ‘await’ planned for C# 5.

LoadDataResult

So, let’s implement result that would load data from a RIA Services context. Create LoadDataResult in Framework as follows:

public class LoadDataResult<TEntity> : IResult where TEntity : Entity
{
	public EntityQuery<TEntity> Query { get; set; }
	public DomainContext Context { get; set; }
	public LoadOperation<TEntity> Result { get; private set; }

	public event EventHandler<ResultCompletionEventArgs> Completed;

	public LoadDataResult(DomainContext context, EntityQuery<TEntity> query)
	{
		Query = query;
		Context = context;
	}

	public void Execute(ActionExecutionContext context)
	{
		Context.Load(Query, LoadDataCallback, null);
	}

	private void LoadDataCallback(LoadOperation<TEntity> data)
	{
		Result = data;
		OnCompleted();
	}

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

Although it might seem complicated, most of the code is just initialization and obvious definitions. The only functions worth attention are Exxecute and LoadDataCallback and these are the same as in the LoadData function. When this result is passed to C.M, it will run Execute and then wait until Completed event is raised. Then it will enumerate next result and so on.

We can replace LoadData and LodDataCallback in ToDoListsViewModel with this function:

public IEnumerable<IResult> LoadData()
{
	CoprojectContext context = new CoprojectContext();
	EntityQuery<ToDoList> query = context.GetToDoListsQuery().OrderByDescending(l => l.CreatedDate);
	var result = new LoadDataResult<ToDoList>(context, query);
	yield return result;

	Lists = result.Result.Entities;
	NotifyOfPropertyChange(() => Lists);
}

Now, although LoadDataResult loads data asynchronously, execution of this function will stop at yield return and continue once the result completes. Workflow of LoadData function is obvious now and can be easily edited later.

Include ToDoItems

We still need to add ToDoItems under their parent ToDoLists. First, we must tell RIA Services to include ToDoItems with lists. Open Services/CoprojectService.cs in Coproject.Web project and this function below GetToDoLists function:

public IQueryable<ToDoList> GetToDoListsWithItems()
{
	return this.ObjectContext.ToDoLists.Include("ToDoItems");
}

ObjectContext is an Entity Framework context, so we instruct EF to include nested ToDoItems when querying underlying database. If these properties are set, RIA Services will automatically include these into metadata transferred to the client ([Include] attribute on respective properties in CoprojectService.metadata.cs that we added in Step 2). Now rebuild the solution in order to have RIA Services recreate context on the client.

Next, point query creation in ToDoListsViewModel.LoadData from context.GetToDoListsQuery to context.GetToDoListsWithItemsQuery.

Finally, edit ToDoListsView and add the following control below the two TextBlocks in ItemTemplate of the ‘Lists’ control:

<ListBox ItemsSource="{Binding ToDoItems}"
				Margin="5,0,0,0" ScrollViewer.HorizontalScrollBarVisibility="Disabled">
	<ListBox.ItemTemplate>
		<DataTemplate>
			<StackPanel Orientation="Horizontal">
				<TextBlock Text="{Binding DueDate,StringFormat='\{0:d\}'}" 
						   FontWeight="Bold" Margin="0,0,5,0" />
				<TextBlock Text="{Binding Content}" />
			</StackPanel>
		</DataTemplate>
	</ListBox.ItemTemplate>
</ListBox>

Now you can run the application. The list will probably run off the screen, so let’s wrap Lists control with ScrollViewer:

<ScrollViewer Grid.Row="1" Style="{StaticResource ListsScrollViewerStyle}">
	<ItemsControl x:Name="Lists">
		...
	</ItemsControl>
</ScrollViewer>

That is all for this step. The application should look like that:
image

Tags: Silverlight, Caliburn, Ria, Coproject

4 Comments

  • Jeff said

    (Sorry for my bad english)
    Thanks for this demo, Its the best caliburn resource that I ever found. I finally understand how to use MEF too, thanks to you.

    I have a doubt, what i need to do if I want to call LoadData() from the OnActivate of my view?

    LoadData().ToSequential().Execute(new ActionExecutionContext());

    Worked fine, is this the right way?

    Again, thanks for you work.

  • Augustin Šulc said

    Hi, Jeff.
    You figured out the same as we did - in part 11, I use

    LoadData().ToSequential().Execute(null);

    to programatically execute the action.

    But whether it is the right way? I don't know :-), but it is the best I've found out so far. If you get something better, let me know!

  • Hennie Bester said

    Hi

    Love this site. Thanks a million.

    What would this look like if it was using the Async CTP?

    Regards

  • Augustin Šulc said

    C.M is not updated to use Async CTP (or .Net 4.5) instead of coroutines, so I haven't updated it yet.

    Nevertheless, instead of LoadDataResult you would probably have a function returning Task<LoadOperation<TEntity>> (created with TaskCompletionSource to wrap the event based async call to RIA). And you would call it from your ViewModel like

    LoadOperation<TEntity> result = await RiaHelper.LoadData(context, query);

    or you might even wrap the logic in an extension function for DomainContext called Load. Thus, DomainContext would seem to support async Task calls:

    LoadOperation<TEntity> result = await context.Load(query);

Add a Comment