Recently I’ve migrated John Papa’s BookShelf (aka BookClub) example to DevForce. Download the DevForce version (BookShelfDF) from our web site. You can learn about John’s original version and get his code from the PDC 2010 web page.
The DevForce BookShelfDF includes a document describing the steps for migrating from RIA Services to DevForce and proceeds to describe the improvements that DevForce made possible. Among them is the ability to create design-time entities for display while developing your Views in visual design tools such as Blend and the Visual Studio designer (“Cider”). This post is a reprint of that section of the document.
Warning: this is a long-ish post.
One way DevForce supports offline scenarios is by making it easy to extract, serialize, and store an EntityManager’s entity cache in a local file. That’s a great way to keep entities – even changed entities – available to an application that must operate offline.
Imagine walking into a customer’s office, prepared to review contracts, previous orders, pending orders, and everything necessary to secure the next big deal. You won’t be plugging into the customer’s network. You may be unable to establish a WiFi or mobile internet connection. But your DevForce-based Silverlight application continues to run with all of the data it needs because you stored a snapshot in isolated storage. You can take the new order and save that to isolated storage as well. You later save the new order to the server when you’ve re-established your connection.
We can use a similar approach using this same technology to support View development with visual design tools such as Blend.
You’ll produce better Views more quickly when you design them in Blend with representative data. Unfortunately, you cannot obtain representative data directly from the database because you cannot make a network connection (even within your development box) within the visual design tools. That’s why the BookShelf uses “Design Models” that create entities in code, on the fly, to populate the repository … and ultimately the ViewModel … when you’re working in Blend.
Writing a few design-entity creation classes as we do in the “DesignModels” folder is easy and it’s a highly recommended approach, especially in the early going before you have a database.
But, as your model grows, you may decide that the number of design creation classes has gotten out of hand. They’ve become yet another burden of classes to maintain. Meanwhile, you have a development database stocked with records that you display and manipulate during dry runs and smoke tests. Can they double as design entities? Yes they can. Follow these two steps:
- Create a “data loader” that reads database entities and emits a file of serialized entities
- Run the loader to create the file
- When working with views inside visual design tools, read and reconstitute this file as design time entities and show them on screen.
Create the Data Loader
The “Data Loader” is a console application that loads selected entities into an EntityManager from the database using a synchronous, 2-tier DevForce client and then saves the manager’s cache (an EntityCacheState or “ECS”) to a local file in the project.
The downloaded example has completed project called “BookShelf.DataLoader”. The ECS file (about about 25KB) is named “DesignCacheState.dat”. It belongs to the project but has a “Build Action” of “None” which means it is not included in the assembly.
The primary steps to building it were:
- Add a full .NET 4 Console Application project called “BookShelf.DataLoader”.
- Add to the project a new text file called “DesignCacheState.txt”
- Rename its extension to “.dat”
- Open its property window and set its “Build Action” to “None”
- Add references
- Create an “app.config” file to tell DevForce how to find the database
- Add a DataLoader class to do the work
- Call the DataLoader in the Main routine of the Program.cs
Steps #5, #6, and #7 invite commentary.
References
The “data loader” needs references to
- The domain model which is defined in the web project in this example
- IdeaBlade DevForce libraries
- Two supporting .NET libraries.
The added references are highlighted as shown:
Remember to set the IdeaBlade library property “Specific Version” to “False” as we have elsewhere.
App.config
The console application runs as a 2-tier, “client/server” application which means the EntityManager in the console application (the client) will make direct contact with the database (the server).
This DevForce client has to know the Entity Framework connection string so that Entity Framework can find its schema files and connect with the database. DevForce looks for this information in the console application’s “app.config” file.
Only the <connectionStrings> element is needed and this we copy from the BookShelf.Web project’s “Web.config” file.
The shipped BookShelf package is configured to access the BookShelf database, located in the BookShelf.Web project’s “App_Data” directory, using SQL Server Express. Therefore, the app.config looks like this (after some reformatting and chopping):
<?xml version="1.0"?>
<configuration>
<connectionStrings>
<!-- SQL Server Connection String -->
<!--<add name="BookClubEntities" connectionString="metadata=res://*/Models.BookClubMo
<!-- SQL Server EXPRESS Connection String from BookShelf.Web/Web.config -->
<add name="BookClubEntities"
connectionString="metadata=res://*/Models.BookClubModel.csdl ...
provider connection string="Data Source=.\SQLEXPRESS;
AttachDbFilename=DataDirectory\BookClub.mdf;
Integrated Security=True;User Instance=True;MultipleActiveResultSets=True""
providerName="System.Data.EntityClient" />
</connectionStrings>
</configuration>
The “DataDirectory” token is a wrinkle dealt with in the DataLoader.
The string refers to the application development database, the source for data while running the application as a developer and soon to be the source for design time data as well. Clearly the string for the production database would be something else in the production Web.config.
DataLoader Class
The “DataLoader” console application is a developer utility. It will never be deployed. Simplicity is the watchword; efficiency, robustness, and architecture are irrelevant.
The DataLoader has a single method, Load, which is called by the static Main in Program.cs:
public void Load()
{
SetDataDirectory();
var context = new BookClubEntities();
// Get all categories, then hold onto the first one
var cat1 = context.Categories.ToList().First();
Console.WriteLine("Loaded categories including " + cat1.CategoryName);
// Get a subset of books belonging to the first category
var books = context.Books.Take(9)
.Where(b => b.CategoryID == cat1.CategoryID)
.ToList();
// Customizing the design data
var firstBook = books.First();
firstBook.Title = "Developers in Wonderland";
firstBook.EntityAspect.AcceptChanges();
Console.WriteLine("Loaded books including " + firstBook.Title);
var bods = context.BookOfDays.Include(b => b.Book).ToList();
Console.WriteLine("Loaded Book-of-the-days including " +
bods.First().Book.Title);
var cos = context.Checkouts.Include(c => c.Book).ToList();
Console.WriteLine("Loaded checkouts including " + cos.First().Book.Title);
context.CacheStateManager.SaveCacheState("../../"+DesignCacheStateFile);
Console.WriteLine("Save design-time ECS as file, "+DesignCacheStateFile);
}
The console application executes four synchronous queries, something a Silverlight application can’t do.
The query results are often typically discarded; we only want the side-effect of filling the entity cache. But the loader can do more than just retrieve database entities. It can add and modify entities programmatically (see the updated title of “firstBook”) to suit specific design needs before writing the cache to file.
Saving the ECS to file
It takes just one line to both extract the EntityCacheState and write it to file:
context.CacheStateManager.SaveCacheState("../../"+DesignCacheStateFile);
The “DesignCacheStateFile” is the name of the text file added to our project earlier. The “SaveCacheState” method would write to the executable directory which is two levels deeper (bin/Debug); the “../../” prefix backs up to the project directory level and overwrites the initial text file. Subsequent re-executions of the loader will overwrite previous versions of the file.
SetDataDirectory
The app.config holds the connection string to the database. This sample app assumes SQL Server Express and ships with a database in the web project; the given connection string instructs SQL Server Express to attach that database dynamically and locates the database symbolically:
AttachDbFilename=DataDirectory\BookClub.mdf;
The “DataDirectory” is a token which is replaced at runtime with the full file location. If we were in the web project, that location would be something like
C:\Users\Ward\Documents\Visual Studio 2010\Projects\
Samples\BookShelf\BookShelfDF\BookShelf.Web\App_Data
That address changes as you move the solution around on your development machine. The “DataDirectory” token accounts for those shifts, leaving the relative part (“\BookClub.mdf”) undisturbed.
Unfortunately, the “DataDirectory” token is null in this DataLoader project. If we do nothing, the console application won’t be able to find the SQL Server Express database and will fail with a helpful error.
The SetDataDirectory method, called at the top of the Load method, determines the file location dynamically and sets the “DataDirectory” token accordingly. See the code for details.
Run The DataLoader
The DataLoader is a utility that you’ll run infrequently compared to running the application or its test. A simple way to run it:
- Right-click the project file in the Visual Studio “Solution Explorer” window
- Select “Debug Start new instance”
The console window shows the loader in action
Visual Studio doesn’t detect the changes to the ECS resource file and won’t know to rebuild the Silverlight application project on its own. You must manually rebuild the Silverlight BookShelf application whenever you run the Data Loader.
The serialized ECS is a product of a specific data model and DevForce version. Changing the model or upgrading DevForce could make the ECS file unreadable. Such vulnerability to change is a minor drawback of the ECS approach.
Use the ECS File for Design-time Entities
The third (and final) step is to retrieve design-time entities from the ECS file while in visual design tools. Please turn your attention to the BookShelf project.
Review the DesignModels folder
The “DesignModels” folder holds the classes that generate collections of entities for View development in visual design tools such as Blend. We’ll replace these design entity sources with the ECS file. We could delete this folder and its contents but we won’t because it’s useful, in a sample application, to show two reasonable ways to achieve this purpose.
Add a linked ECS file as a Resource
The Silverlight application needs access to the ECS file when open in Blend. We link to it much as we linked earlier to the generated entity class file to get a Silverlight version of the domain model in the web project.
- Select the DesignServices folder in the “Solution Explorer” window
- Pick “Add Existing item” (Shift – Alt – A)
- Navigate to the BookShelf.DataLoader project
- Select the ECS file, “DesignCacheState.dat”
- Click the drop-down arrow next to the “Add” button
- Click “Add As Link”
A shortcut to that file appears among the members of the DesingServices folder:
Set the linked file’s “Build Action” to “Resource”.
Revise the DesignEntityManagerProvider
The design entities we see in Blend are created within the DesignEntityManagerProvider located in the DesignServices folder.
This provider, when it created entities from Design Model classes, looked like this:
public class DesignEntityManagerProvider : IEntityManagerProvider
{
public BookClubEntities CreateManager()
{
var context = new BookClubEntities(false /* offline mode */);
// Populate cache with design data
PopulateFromDesignModels(context);
return context;
}
/// <summary>
/// Populate the manager from design-time entities created by code.
/// </summary>
private static void PopulateFromDesignModels(EntityManager context)
{
context.AttachEntities(new DesignCategories());
context.AttachEntities(new DesignBooks());
context.AttachEntities(new DesignCheckouts());
context.AttachEntity(new BookOfDay
{
Day = DateTime.Today,
Book = DesignBooks.ExampleBook,
});
}
It created the BookShelf’s EntityManager (BookShelfEntities) and attached design-time entities to that manager – entities created in code.
Comment out the call to that method and add a call to a new method,
// Populate cache with design data
// PopulateFromDesignModels(context);
PopulateFromEntityCacheState(context);
The “populator” that draws upon the ECS file takes a tad more work to set up.
It might not seem worthwhile in an application this size although it’s easy to forget the combined 100+ lines in the three Design Model classes. The ECS file approach really pays off when the application model has more than four entity types and the relationships among the entities are numerous and complex.
The ECS populator seen here remains the same whether the model is four entities or 400 entities. It proceeds in three stages:
- Tell DevForce where to find the assembly with the BookShelf model in the static constructor.
- Create an in-memory EntityCacheState object from the ECS file.
- Populate the design-time EntityManager from the in-memory EntityCacheState object.
static DesignEntityManagerProvider()
{
// Register the model's assembly name among probed assemblies
// Must be called BEFORE the first EM creation else
// GetDesignEntityCacheState fails w/ deserialization exception
IdeaBlade.Core.IdeaBladeConfig.Instance.ProbeAssemblyNames
.Add(typeof(BookClubEntities).Assembly.FullName);
}
/// <summary>
/// Get the design-time <see cref="EntityCacheState"/> from resource file
/// </summary>
private static EntityCacheState GetDesignEntityCacheState()
{
const string filename =
@"/BookShelf;component/DesignServices/DesignCacheState.dat";
var res = Application.GetResourceStream(new Uri(filename, UriKind.Relative));
return _designEntityCacheState = EntityCacheState.Restore(res.Stream);
}
private static EntityCacheState _designEntityCacheState;
/// <summary>
/// Populate the manager from design-time entities held in
/// an in-memory <see cref="EntityCacheState"/>.
/// </summary>
private static void PopulateFromEntityCacheState(EntityManager context)
{
var ecs = _designEntityCacheState ?? GetDesignEntityCacheState();
context.CacheStateManager.RestoreCacheState(ecs, RestoreStrategy.Normal);
}
Here’s the BookView as seen in Blend, showing the design data sourced from the ECS file
Hope you found this discussion illuminating. Happy Coding!
2 comments:
Hello,
the Download-Link http://www.blogger.com/www.ideablade.com/BookShelfDF/BookShelfDF.zip won't work.
Regards
Dirk
Thanks, Dirk. I've fixed it.
Post a Comment