In This Chapter
• Code-first and database-first data model creation
• Differences between isolated storage and XAP file deployment
• Change tracking with LINQ to SQL
• Defining one-to-many relationships
• Retrieving a local database from isolated storage
• Using SqlMetal to generate your data model
• Deploying a prepopulated database to isolated storage
• A testable navigation service
• Mapping inheritance hierarchies
• Controlling optimistic concurrency
• A Twitter timeline viewer
For some apps, having to rely on a cloud service for storing application data is not practical. Some scenarios cry out for local storage of structured data on the phone.
With the Mango release of Windows Phone (7.5), Microsoft introduced a relational database system to the phone, allowing you to place a local database in either isolated storage or in your app’s XAP file.
Windows Phone apps use LINQ to SQL for all database operations; LINQ to SQL is used to define the database schema, and to select, insert, update, and delete data from the database. LINQ to SQL provides an object-oriented approach to working with relational data and is comprised of an object model and a runtime. With LINQ to SQL, classes represent database tables, and foreign key relationships between tables are represented by aggregated collections.
This chapter begins with an overview of how local databases are deployed to the phone and discusses the main elements of LINQ to SQL.
The chapter then explores the code-first approach to data model creation and walks through a sample Twitter timeline viewer that caches data in a local database, during which you explore several facets of LINQ to SQL including entity creation, change tracking, and entity multiplicity (one-to-many relationships).
The database-first approach to data model creation is then discussed. You see how to retrieve a local database file from isolated storage and how to use the SqlMetal tool to generate a data model from a database.
The chapter then takes a short deviation and examines a custom navigation service that allows navigation from a viewmodel without being coupled to the built-in navigation of the phone, which can increase the testability of your viewmodel classes.
The chapter then gets back on topic to examine a technique for observing LINQ to SQL queries using a custom log. You see how to upgrade a schema without losing data and how to use version numbering to provide incremental updates to your database schema.
Finally, you look at mapping inheritance hierarchies using LINQ to SQL, and at concurrency and conflict detection when performing updates.
The database used on the phone is based on SQL Server Compact Edition (SQL CE). SQL CE shares a common API with the other Microsoft SQL Server editions. Each SQL CE database resides in a single .sdf file, which can be up to 4GB in size. The .sdf file can be encrypted, using a password, with 128-bit encryption for data security.
On Windows Phone, the database filename is required to use the .sdf file extension. A NotSupportedException
is raised if you attempt to create a database with a name not ending in .sdf.
Understanding how local databases can be deployed to the phone is important when determining the best approach for your app. This section looks behind the scenes at what happens when an app is installed on the phone and at the two locations for local database deployment.
It so happens that a XAP file, which is produced when you build your project, is also a Zip file. When an app is installed on the phone, the package manager extracts the app’s XAP file into an install directory (see Figure 26.1).
For more information on XAP files see the section “Understanding the Role of XAP Files” in Chapter 2, “Fundamental Concepts in Silverlight Development for Windows Phone.”
The install directory is read-only. An app is not able to add or remove items from its install directory, nor is it able to make any changes to any items in the directory.
During installation, the app is also assigned a data directory. The contents of this directory and all subdirectories are readable and writable by the app.
A local database can be placed either in isolated storage or in the app’s install directory by including it within the XAP. You see why the choice of where to place a local database is important in a moment.
An app’s data directory is persistent, while its install directory is not. When publishing an update for your app on the Windows Phone Marketplace, the install directory of your app is replaced, but the data directory for your app remains intact.
Moreover, when placed in the install directory, a database is read-only. This suits certain scenarios, such as when you want to include structured reference data with your app, data that may change with each application update, but that is not modifiable by the app itself. An example of this kind of data is a dictionary of word definitions.
The most common place for a database, however, is in isolated storage. When in isolated storage the database is readable and writable, and is retained during an app update.
There are two common scenarios for deploying a database to isolated storage. The first is where your app has a complex domain model and you want to persist its state. Perhaps your app is a shopping cart or a todo list. The second scenario is where you fetch a subset of reference data from the cloud, cache it locally, and combine it with user-specific data.
This chapter demonstrates each deployment option and focuses on using a local database as a cache by creating a Twitter app that downloads timeline information and caches it in a database so that it is available when the phone is offline.
Retrieval and storage of data in a local database is done entirely using LINQ to SQL, which provides object-relational mapping (ORM) capabilities, allowing you to use LINQ to communicate with a relational database. LINQ to SQL maps the object model, which you define by decorating entity classes with attributes, to a relational database. When your app runs, LINQ to SQL translates language-integrated queries into Transact-SQL and then sends the queries to the database for execution. When the database returns the results, LINQ to SQL translates the results back to objects.
When first learning of Microsoft’s choice to go with LINQ to SQL in Windows Phone, I was somewhat dismayed that it was chosen over the Entity Framework. Yet, while the Entity Framework offers a more powerful API and set of tools, it is not optimized for mobile devices. I have since come to see LINQ to SQL as a good fit for the phone, as it offers a simple API for rapid data layer construction.
LINQ to SQL makes heavy use of the System.Data.Linq.DataContext
class, which serves as a proxy to your database, allowing you to query the database via Table
objects representing tables in the database (see Figure 26.2). Each Table
member of a DataContext
allows the storage and retrieval of entity objects; an instance of an entity represents a table row. Persisting changes to the local database is done using the DataContext.SubmitChanges
method.
By default, DataContext
also tracks changes to your entity objects so that you can easily push changes back to your database.
An entity is a POCO (Plain Old CLR Object), representing a domain object, and is decorated with a System.Data.Linq.Mapping.Table
attribute. Other data related attributes, placed on and within each entity class, define the mapping between the object model and the schema of the database and determine the structure of the database.
Windows Phone supports most of the features of LINQ to SQL, apart from several notable absences present on other platforms, and some additional features not seen in LINQ to SQL on other platforms. The following is a list of some of the most notable differences:
• LINQ to SQL on Windows Phone does not directly support the execution of Transact-SQL, which includes Data Definition Language (DDL) and Data Modeling Language (DML) statements.
• Local databases run in your app’s process and not in a background process, as is the case with most desktop based databases.
• A local database cannot be accessed by other apps; it lives in the sandboxed execution environment of your app.
• Transactions are not directly supported.
For more detailed information on the various API differences see http://bit.ly/xQMxmm.
New features have also been added to LINQ to SQL, specifically for Windows Phone apps. These include support for multiple table indexes and the ability to programmatically alter the database schema (described later in this chapter).
Using LINQ to SQL in your phone app requires referencing the assembly System.Data.Linq
, which is included in the Windows Phone Framework Class Library (FCL).
There are two commonly used approaches to the creation of an application’s data model: code-first and database-first. Code-first entails writing your classes first and then using those classes to generate the database. Conversely, database-first entails creating your database schema and then using a tool to generate an object model for interacting with the database. The database-first approach is discussed later in the chapter. For now, though, we concentrate on the code-first approach.
Code-first is the recommended approach for creating your data model in Windows Phone. It involves writing your classes and then generating your database using metadata contained in those classes.
Until now, I have never been a fan of code-first when building desktop and client-server applications because I saw how it can lead to a lack of attention being paid to the database schema—to its structure, constraints, and indexes. Yet, applied wisely, code-first offers some key advantages. One such advantage is that you do not break the application by forgetting to propagate a schema change to other developers on your team, since all changes to the schema are done via code metadata.
The following list outlines the steps for creating a data model and generating a database using the code-first approach and LINQ to SQL:
1. Manually create POCO classes representing your domain entities.
2. Create a class deriving from DataContext
, with properties within the class representing tables within the database.
3. When instantiating your DataContext
, provide a connection string with the location of your database file in isolated storage. The DataContext
creates a database with the relationships defined in your data model.
Once the database is in place, you are able to interact with the DataContext
and the entity objects using LINQ to SQL.
The following section looks at each of these steps in detail during the creation of a sample Twitter timeline viewer.
The principle example for this chapter is a Twitter timeline viewer that downloads the tweets of a specified Twitter user, caches them in a local database, and displays them on a page. The code for this example is located in the DataDrivenApps/TwitterExample directory of the WindowsPhone7Unleashed.Examples project.
The Twitter timeline viewer has a simple data model consisting of two entities representing a Twitter user and a tweet or TimelineItem
(see Figure 26.3), and demonstrates using a local database to cache cloud data.
TimelineItem
represents a status update (tweet) that contains information including the tweet text, the time the tweet occurred, and the user’s id (see Listing 26.1). The TwitterUser
class holds a collection, specifically an EntitySet
of TimelineItem
objects.
[Table]
public class TimelineItem : NotifyPropertyChangeBase
{
string id;
[Column(IsPrimaryKey = true)]
public string Id
{
get
{
return id;
}
set
{
Assign("Id", ref id, value);
}
}
string text;
[Column(DbType = "NVarChar(140)")]
public string Text
{
get
{
return text;
}
set
{
Assign("Text", ref text, value);
}
}
DateTime? receivedTime;
[Column]
public DateTime? ReceivedTime
{
get
{
return receivedTime;
}
set
{
Assign("ReceivedTime", ref receivedTime, value);
}
}
string twitterUserId;
[Column(CanBeNull = false)]
public string TwitterUserId
{
get
{
return twitterUserId;
}
set
{
Assign("TwitterUserId", ref twitterUserId, value);
}
}
EntityRef<TwitterUser> user;
[Association(Storage = "user",
ThisKey = "TwitterUserId",
IsForeignKey = true)]
public TwitterUser TwitterUser
{
get
{
return user.Entity;
}
set
{
user.Entity = value;
}
}
}
To declare the TimelineItem
class as representing a table in the database, it is decorated with a Table
attribute. The TableAttribute
class includes a Name
property, which allows you to explicitly define the name of the database table. If it is not provided, the name of the class is used as the table name. For example, to override the TimelineItem
table name to something else, the following could be used:
[Table(Name = "SomethingElse")]
public class TimelineItem : NotifyPropertyChangeBase
{ ... }
Each property of the TimelineItem
is decorated with a Column
attribute, which tells the DataContext
that the property is stored in a column by the same name. As with TableAttribute
, you can use its Name
property to specify a different name for the column in the database.
ColumnAttribute
includes various other properties described in the following sections.
IsPrimaryKey
is used to designate the member or members of the entity class that comprise the primary key of the table. Composite keys are also supported by supplying more than one property decorated with a Column
attribute with IsPrimaryKey
set to true.
At least one member of an entity class must be designated as the primary key or else the entity is deemed to be read-only. If an attempt is made to create, update, or delete a read-only entity, an InvalidOperationException
is raised.
AutoSync
is used to specify whether the column is automatically synchronized from the value generated by the database on insert or update commands.
The following is a list of valid values for this property:
• Default
• Always
• Never
• OnInsert
• OnUpdate
If the column in your database provides a default value, set this property to OnInsert
, OnUpdate
, or Always
.
CanBeNull
indicates whether a column can contain null values. This property is relevant when inserting data into the database but not during the creation of the table.
The DataContext.CreateDatabase
method uses only the ColumnAttribute.DbType
property and not the ColumnAttribute.CanBeNull
property when determining whether a column should allow null values. For this reason, you must specify whether a column can contain null values in the DbType
property as well as setting CanBeNull
to true.
If not specified, CanBeNull
defaults to true if the member is a reference type; if the member is a value type, such as an int
, then CanBeNull
defaults to false.
DbType
specifies the text that defines the column in a Transact-SQL create table statement. Using this property allows you to tailor a column data type to better match the data being stored in the column. If used correctly, it can help to decrease the size of your database and may improve the speed of some queries.
For example, by not specifying the DbType
of a string property, LINQ to SQL generates DDL for a column containing up to 4000 Unicode characters (NVARCHAR(4000)). If you, however, know that a field will always contain values of relatively the same length (within two characters difference), then NCHAR can be faster and more space efficient for data, and it may pay dividends to specify it using DbType
.
SQL CE is a Unicode-only database so you must use Unicode data types when assigning the DbType
property. In particular, use NVARCHAR instead of VARCHAR, and NCHAR instead of CHAR.
Expression
allows you to automatically populate a field using a SQL expression. Use this property to define a column as containing computed values when you use DataContext.CreateDatabase
to generate your database.
For example, if you want to create a column defined in SQL as InventoryValue AS UnitPrice * UnitsInStock, use the following Expression
string:
UnitPrice * UnitsInStock
Entity members with IsDbGenerated
are synchronized immediately after the row of data representing the entity is inserted into the database. The entity member’s value is set when DataContext.SubmitChanges
completes.
IsDescriminator
is used in conjunction with the InheritanceMapping
attribute to provide an inheritance hierarchy in your data model. For more information, see the section “Mapping an Inheritance Hierarchy,” later in this chapter.
IsVersion
allows you to designate an entity class member to be used for optimistic concurrency control (OCC). This property is discussed in the later section “Concurrency.”
UpdateCheck
tells the data context when to detect change conflicts. This property is discussed in the later section “Concurrency.”
The dirty state of an entity is monitored by the data context. This is usually accomplished with change notifications, provided through the INotifyPropertyChanging.PropertyChanging
and INotifyPropertyChanged.PropertyChanged
events, raised in the property setters of the entity.
Combining the PropertyChanged
and PropertyChanging
events raising into every property setter can add up to a lot of plumbing code. Fortunately, the Assign
method in the custom ViewModelBase
class used throughout this book raises the events required for change tracking.
For more information on the property change notification, in relation to the ViewModelBase
class, see Chapter 2.
For entities that do not implement INotifyPropertyChanging
, LINQ to SQL maintains a copy of their values. When DataContext.SubmitChanges
is called, the current and original values are compared to determine whether the object has changed.
To decrease the amount of memory used by your app, ensure that entity classes implement INotifyPropertyChanged
and INotifyPropertyChanging
.
The TwitterUser
class contains various properties, such as the screen name of the Twitter user. It also includes a collection of TimelineItem
objects that are maintained using an EntitySet
. An EntitySet
provides for deferred loading and manages the relationship of the collection side of one-to-many relationships between entities.
The TwitterUser
constructor initializes the EntitySet
, specifying two handlers: one that is called when a TimelineItem
is added to its collection; the other, when a TimelineItem
is removed (see Listing 26.2).
The TwitterUser
class also uses the Index
attribute, which tells the DataContext
to create a database index for the secondary column ScreenName.
Indexes improve the speed of data retrieval operations by allowing the database engine to quickly locate a record. Without an index, potentially all rows in a table need to be scanned, an expensive O(n) operation.1
1 O(n) describes algorithm complexity. For more information on Big O notation see http://en.wikipedia.org/wiki/Big_O_notation.
[Table]
[Index(Columns = "ScreenName")]
public class TwitterUser : NotifyPropertyChangeBase
{
public TwitterUser()
{
timelineItems = new EntitySet<TimelineItem>(
AttachTimelineItem, DetachTimelineItems);
}
string id;
[Column(IsPrimaryKey = true)]
public string Id
{
get
{
return id;
}
set
{
Assign("Id", ref id, value);
}
}
string screenName;
[Column]
public string ScreenName
{
get
{
return screenName;
}
set
{
Assign("ScreenName", ref screenName, value);
}
}
... (ImageUrl and Description properties omitted)
readonly EntitySet<TimelineItem> timelineItems;
[Association(
Storage = "timelineItems",
OtherKey = "TwitterUserId")]
public EntitySet<TimelineItem> TimelineItems
{
get
{
return timelineItems;
}
set
{
timelineItems.Assign(value);
}
}
void AttachTimelineItem(TimelineItem entity)
{
OnPropertyChanging("TimelineItems", timelineItems, timelineItems);
entity.TwitterUser = this;
OnPropertyChanged("TimelineItems");
}
void DetachTimelineItems(TimelineItem entity)
{
OnPropertyChanging("TimelineItems", timelineItems, null);
entity.TwitterUser = null;
OnPropertyChanged("TimelineItems");
}
}
The PropertyChanging
event is raised manually before the assignment of the TimelineItem.TwitterUser
property, and the PropertyChanged
event is raised after it has been assigned. These calls indicate to the change tracking infrastructure that the TwitterUser
object is dirty and differs from the stored value in the database.
One-to-many entity relationships can be defined by decorating entity properties with Association
attributes. Two entities participating in a one-to-many relationship with each other each use properties of the Association
attribute to indicate on which side each resides.
One-to-many entity relationships are represented as foreign key relationships in the database.
The one-to-many relationship between TwitterUser
and TimelineItem
is defined with an Association
attribute placed on the TwitterUser
property of the TimelineItem
class. The attribute declares that the TwitterUserId
column of the TimelineItem
table is a foreign key of the TwitterUser
table. It also declares that the user field holds a reference (an EntityRef
) to a TwitterUser
object.
Conversely, on the many side of the relationship, the TwitterUser
property of the TimelineItem
declares that the TwitterUserId
property holds the foreign key of the TwitterUser
.
An EntityRef
represents the one side of a one-to-many relationship, while an EntitySet
is the reciprocal and represents the many side.
The TwitterDataContext
class is the proxy to the Twitter local database and includes two public Table
<
TSource
>
properties: TwitterUsers
and TimelineItems
(see Listing 26.3).
The assignment of the DataContext.Log
property allows you to monitor the activities of the DataContext
at runtime, and in particular, to observe the generated SQL as it is being sent to the database. This customized logging is examined later in the chapter.
The GetTable
property of the DataContext
class retrieves a Table<TEntity>
object representing a table in the underlying database. Table
objects can be queried using LINQ to SQL.
public class TwitterDataContext : DataContext
{
public TwitterDataContext(string connection) : base(connection)
{
Log = new DebugStreamWriter();
}
public Table<TwitterUser> TwitterUsers
{
get
{
return GetTable<TwitterUser>();
}
}
public Table<TimelineItem> TimelineItems
{
get
{
return GetTable<TimelineItem>();
}
}
}
The Table<TEntity>
class allows you to perform CRUD (create, read, update, delete) operations on a table. For example, you can insert a row (representing an entity) into the underlying table using the Table.InsertOnSubmit(TEntity entity)
method.
The Table<TEntity>
class also allows you to attach a disconnected (or detached) entity to a new DataContext
.
As an aside, the DataContext
base class automatically initializes its public fields of type Table<TEntity>
. By placing a public field in a DataContext
derived class, such as the following, the field is automatically assigned when the DataContext
is instantiated:
public Table<TwitterUser>;
While this offers a way of exposing Table
objects from your DataContext
using less code, the use of public fields is not recommended because it breaks the convention that fields should have private visibility; nonprivate fields violate the encapsulation of the class and make the class less amenable to change.
While DataContext
enables you to create and delete a database file, a custom class, called DatabaseUtility
, is used to directly create and delete the database in isolated storage and to build the connection string for the database.
TwitterDatabaseUtility
extends the DatabaseUtility
class, providing the default database filename. TwitterDatabaseUtility
is used in various locations to create a DataContext
for the Twitter database.
DatabaseUtility
includes an InitializeDatabase
method that creates the isolated storage file for the database. When it is called, if the wipe
parameter is true and the database already exists, then it is deleted before being re-created. See the following excerpt:
public void InitializeDatabase(DataContext dataContext, bool wipe = false)
{
ArgumentValidator.AssertNotNull(dataContext, "dataContext");
if (localDatabaseMode != LocalDatabaseMode.ReadWrite
&& localDatabaseMode != LocalDatabaseMode.Exclusive)
{
return;
}
if (wipe && dataContext.DatabaseExists())
{
dataContext.DeleteDatabase();
}
if (!dataContext.DatabaseExists())
{
dataContext.CreateDatabase();
}
}
Initialization of the local database occurs when the Launching
event of the PhoneApplicationService
is raised. For testing purposes, if a debugger is attached, the database is deleted so as to begin with a clean slate. The following excerpt shows the Launching
event handler:
void Application_Launching(object sender, LaunchingEventArgs e)
{
Debug.WriteLine("Application Launching");
StateManager.Initialize();
/* For the Twitter demo. */
var twitterDatabaseUtility = new TwitterDatabaseUtility();
bool wipeDatabase = Debugger.IsAttached;
twitterDatabaseUtility.InitializeDatabase(wipeDatabase);
}
When the app is launched, the handler ensures that the database file is created in isolated storage.
When instantiating a DataContext
, a connection string must be supplied, indicating the location of the database file and an optional password. See the following example:
string connectionString
= "Data Source='isostore:/DirectoryName/FileName.sdf';Password='password'";
DataContext dataContext = new DataContext(connectionString);
The Data Source parameter defines the location of the database file in isolated storage, while the Password parameter is used to access (or create) an encrypted database.
The isostore prefix of the Data Source value specifies that the file is located in isolated storage. By omitting the isostore prefix, it indicates that the file is located in the XAP file relative to its root directory. You see an example of using a database located in the XAP file later in the chapter.
Table 26.1 describes the database connection string parameters in more detail.
A database must be encrypted when it is created; a password cannot be used to encrypt the database after it has been created.
The ConnectionString
property of the custom DatabaseUtility
class constructs a connection string using its DatabasePassword
property, its DataSource
property, and the localDatabaseMode
field represented as a custom enum value. See the following excerpt:
public string ConnectionString
{
get
{
if (string.IsNullOrWhiteSpace(DatabasePassword))
{
return string.Format("Data Source='{0}';Mode={1}",
DataSource, localDatabaseMode.ToConnectionStringValue());
}
return string.Format("Data Source='{0}';Password='{1}';Mode={2}",
DataSource,
DatabasePassword,
localDatabaseMode.ToConnectionStringValue());
}
}
The following excerpt shows the LocalDatabaseMode
enum, which represents the four connection string Mode
values that were listed in Table 26.1:
public enum LocalDatabaseMode
{
ReadWrite,
ReadOnly,
Exclusive,
SharedRead
}
An extension method is used to convert an enum value to a valid Mode
value. Using an enum, combined with an extension method for conversion, is an elegant alternative to loosely typed strings. The following excerpt shows the ToConnectionStringValue
method:
public static class LocalDatabaseModeExtensions
{
public static string ToConnectionStringValue(
this LocalDatabaseMode mode)
{
switch (mode)
{
case LocalDatabaseMode.ReadWrite:
return "Read Write";
case LocalDatabaseMode.ReadOnly:
return "Read Only";
case LocalDatabaseMode.Exclusive:
return "Exclusive";
case LocalDatabaseMode.SharedRead:
return "Shared Read";
default:
throw new ArgumentException(
"Unknown mode: " + mode);
}
}
}
To target a database file in your app’s XAP file, do not include the isostore
prefix in the Data Source segment of its connection string. The following example shows a connection string for an .sdf file located in a XAP file:
string connectionString
= "Data Source='/ProjectDirectoryName/FileName.sdf';Mode=Read Only";
DataContext dataContext = new DataContext(connectionString);
The connection string for a database located in your XAP file must use the Mode of Read Only; otherwise a System.Data.SqlServerCe.SqlCeException
is raised when attempting to access the database, regardless of whether your query is only attempting to read, such as with a select operation.
The custom DatabaseUtility
class takes care of building the connection string. A preprocessor directive in the TwitterDatabaseUtility
class allows you to change the location of the Twitter database to the XAP file. If enabled, the Read Only mode is automatically introduced into the connection string. Being in read-only mode, however, breaks the app, because the TwitterService
needs to write to the database.
A custom Twitter service is used for all communication with the Twitter web API. The service also provides the ability to cache the results of calls to Twitter using a local database.
The viewmodel, which presents the Twitter timeline information, consumes the Twitter service using an ITwitterService
interface.
The ITwitterService
contains two methods designed to execute asynchronously. They are as follows:
• GetTimeline(
string
screenName,
Action
<
ResultEventArgs
<
TwitterUser
>> completeAction)
• GetCachedTimeline(
string
screenName,
Action
<
ResultEventArgs
<
TwitterUser
>> completeAction)
A caller of either method provides an Action
argument, which, on completion of the method, is invoked, notifying the caller of the result.
The ResultEventArgs
class either contains the result produced by the operation, or an Exception
object. ResultEventArgs
is presented in Chapter 24, “Network Services.”
GetTimeline
uses the Twitter web API to retrieve a Twitter user’s details and her list of status updates. Once a response is received from the Web, the result is converted into entity objects representing the Twitter user and the status updates (TimelineItem
objects).
GetCachedTimeline
is used to retrieve a TwitterUser
from the database. This method does not produce a result until GetTimeline
has successfully completed at least once because until then no data has been placed in the database.
The default implementation of the ITwitterService
is the TwitterService
class. TwitterService
relies on the TwitterDatabaseUtility
. A TwitterDataContext
is created when the TwitterService
is instantiated, as shown:
public class TwitterService : ITwitterService
{
readonly TwitterDatabaseUtility twitterDatabaseUtility
= new TwitterDatabaseUtility();
readonly TwitterDataContext dataContext;
public TwitterService()
{
dataContext = twitterDatabaseUtility.CreateContext();
}
...
}
The public GetTimeline
method queues the Twitter call on a ThreadPool
thread. To demonstrate population using the local database, the ThreadPool
thread is made to wait for 3 seconds before calling the main GetTimelineCore
method. If an exception is raised, the complete action is called, as shown:
public void GetTimeline(
string screenName, Action<ResultEventArgs<TwitterUser>> completeAction)
{
ArgumentValidator.AssertNotNullOrWhiteSpace(screenName, "userName");
ArgumentValidator.AssertNotNull(completeAction, "completeAction");
ThreadPool.QueueUserWorkItem(
delegate
{
try
{
/*Wait for a moment to demonstrate the database retrieval.*/
Wait(3000); /* Equivalent to Thread.Sleep(3000); */
GetTimelineCore(screenName, completeAction);
}
catch (Exception ex)
{
Console.WriteLine(ex);
completeAction(new ResultEventArgs<TwitterUser>(null, ex));
}
});
}
GetTimelineCore
uses the Twitter web API (see Listing 26.4). A WebClient
is used to dispatch an HTTP request to the site. When the call returns it is handled within a lambda expression. The result of the call is an XML fragment, which we convert to an XElement
using the XElement.Parse
method.
The user information is then extracted from the root element before building a list of TimelineItems
. Finally, the data context is used to store the TwitterUser
and its associated TimelineItems
in the database.
void GetTimelineCore(
string screenName, Action<ResultEventArgs<TwitterUser>> completeAction)
{
WebClient twitterClient = new WebClient();
twitterClient.DownloadStringCompleted
+= (o, args) =>
{
if (args.Error != null)
{
completeAction(
new ResultEventArgs<TwitterUser>(null, args.Error));
return;
}
try
{
XElement rootElement = XElement.Parse(args.Result);
XElement element = rootElement.Descendants("status").First();
XElement userElement = element.Descendants("user").First();
var twitterUser = new TwitterUser
{
Id = userElement.Element("id").Value,
ScreenName = userElement.Element("screen_name").Value,
Description = userElement.Element("description").Value,
ImageUrl = userElement.Element("profile_image_url").Value,
};
IEnumerable<TimelineItem> items = from statusElement
in rootElement.Descendants("status")
select new TimelineItem
{
Id = statusElement.Element("id").Value,
ReceivedTime = ConvertToDateTime(
statusElement.Element("created_at").Value),
Text = statusElement.Element("text").Value
};
TwitterUser storedUser = dataContext.TwitterUsers.Where(
user => user.ScreenName == screenName).FirstOrDefault();
if (storedUser != null)
{
dataContext.TimelineItems.DeleteAllOnSubmit(
dataContext.TimelineItems.Where(
item => item.TwitterUserId == storedUser.Id));
dataContext.TwitterUsers.DeleteOnSubmit(storedUser);
dataContext.SubmitChanges();
}
twitterUser.TimelineItems.AddRange(items);
dataContext.TwitterUsers.InsertOnSubmit(twitterUser);
dataContext.SubmitChanges();
completeAction(new ResultEventArgs<TwitterUser>(twitterUser));
}
catch (Exception ex)
{
Debug.WriteLine("Unable to get timeline. " + ex);
completeAction(new ResultEventArgs<TwitterUser>(null, ex));
return;
}
};
twitterClient.DownloadStringAsync(new Uri(
"http://api.twitter.com/1/statuses/user_timeline.xml?screen_name="
+ screenName));
}
Once the Twitter web API has been queried and the result stored in the database, the cached data can be retrieved from the database. This allows the Twitter timeline information to remain viewable when no network connection is available.
Changes to the entity model are not persisted to the database until DataContext.SubmitChanges
is called. New entity objects that have been added to the data context are not inserted into the database until SubmitChanges
is called, and entities that have been removed from the data context are not deleted from the database until SubmitChanges
is called.
Retrieving the cached data from the local database is the task of the TwitterService
object’s GetCachedTimeline
method.
You see the same pattern applied as earlier; the principle GetCachedTimelineCore
method is called from a ThreadPool
thread as shown:
public void GetCachedTimeline(
string screenName, Action<ResultEventArgs<TwitterUser>> completeAction)
{
ArgumentValidator.AssertNotNullOrWhiteSpace(screenName, "userName");
ArgumentValidator.AssertNotNull(completeAction, "completeAction");
ThreadPool.QueueUserWorkItem(
delegate
{
try
{
GetCachedTimelineCore(screenName, completeAction);
}
catch (Exception ex)
{
Console.WriteLine(ex);
completeAction(new ResultEventArgs<TwitterUser>(null, ex));
}
});
}
The GetCachedTimelineCore
method uses the data context to retrieve the Twitter user with the specified screen name, shown in the following excerpt:
void GetCachedTimelineCore(
string userName, Action<ResultEventArgs<TwitterUser>> completeAction)
{
TwitterUser storedUser = dataContext.TwitterUsers.Where(
user => user.ScreenName == userName).FirstOrDefault();
completeAction(new ResultEventArgs<TwitterUser>(storedUser));
}
You saw how the TwitterService
is able to retrieve live data from the Twitter web API and then cache it using a local database. Subsequent sections look at presenting this data to the user.
The TwitterSignInView
and associated viewmodel provide the UI for gathering the user’s Twitter credentials. The sample, however, does not actually require the user to sign in to Twitter because we are merely retrieving the timeline for the user, and this does not require authentication.
A sign-in screen is included to provide the infrastructure to extend the app in the direction of being a fully blown Twitter app. It also provides a nice segue to demonstrate the custom INavigationService
, which is discussed later in the chapter.
The TwitterSignInViewModel
class contains properties for the user’s Twitter credentials and a property that causes the password to be retained by the app (see Listing 26.5).
The viewmodel relies on the ViewModelBase
class for state persistency. The Username
, Password
, and RememberPassword
properties are each decorated with the custom State
attribute so that they are persisted to isolated storage. For details on the custom state preservation system, see Chapter 25, “Isolated Storage and State Preservation.”
When executed, the viewmodel’s SignInCommand
is responsible for navigating to the TwitterTimeline
page and for deregistering state preservation for the Password property if the user has indicated that the app should not remember the password.
The Navigate
method and the custom navigation functionality are discussed later in this chapter.
public class TwitterSignInViewModel : ViewModelBase
{
public TwitterSignInViewModel()
{
signInCommand = new DelegateCommand(
obj =>
{
if (!rememberPassword)
{
DeregisterStatefulProperty(
ApplicationStateType.Persistent, () => Password);
}
Navigate("/TwitterTimeline/" + username);
});
}
string username;
[Stateful(ApplicationStateType.Persistent)]
public string Username
{
get
{
return username;
}
set
{
Assign(() => Username, ref username, value);
}
}
string password;
[Stateful(ApplicationStateType.Persistent)]
public string Password
{
get
{
return password;
}
set
{
Assign(() => Password, ref password, value);
}
}
readonly DelegateCommand signInCommand;
public ICommand SignInCommand
{
get
{
return signInCommand;
}
}
bool rememberPassword = true;
[Stateful(ApplicationStateType.Persistent)]
public bool RememberPassword
{
get
{
return rememberPassword;
}
set
{
Assign(() => RememberPassword, ref rememberPassword, value);
}
}
}
Within the view’s XAML, an AppBarIconButton
is bound to the SignInCommand
.
A ToggleSwitch
from the Silverlight Toolkit is used to set the RememberPassword
property of the viewmodel. See the following excerpt:
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<u:AppBar>
<u:AppBarIconButton
Command="{Binding SignInCommand}"
Text="Sign In"
IconUri="/Images/ApplicationBarIcons/ApplicationBar.Check.png" />
</u:AppBar>
<StackPanel Grid.Row="0" Style="{StaticResource PageTitlePanelStyle}">
<TextBlock Text="Windows Phone 7 Unleashed"
Style="{StaticResource PhoneTextAppTitleStyle}" />
<TextBlock Text="Twitter sign in"
Style="{StaticResource PhoneTextPageTitleStyle}" />
</StackPanel>
<StackPanel x:Name="ContentPanel" Grid.Row="1"
Margin="{StaticResource PhoneHorizontalMargin}">
<TextBlock Text="username"
Style="{StaticResource LabelTextIndentedStyle}" />
<TextBox Text="{Binding Username, Mode=TwoWay}" />
<TextBlock Text="password"
Style="{StaticResource LabelTextIndentedStyle}" />
<!--<PasswordBox Password="{Binding Password, Mode=TwoWay}" />-->
<TextBox Text="not required in this example." IsReadOnly="True" />
<toolkit:ToggleSwitch
IsChecked="{Binding RememberPassword, Mode=TwoWay}"
Header="Remember Password" />
</StackPanel>
</Grid>
Tapping the application bar button executes the SignInCommand
, which causes the app to navigate to the timeline view page (see Figure 26.4).
If the app happens to be tombstoned or exited, the state of the various controls is retained in persistent storage.
Tapping the ApplicationBarIconButton
navigates the app to the timeline view page.
The timeline view presents a Twitter user’s list of recent tweets (status updates). The TwitterTimelineViewModel
class relies on the ITwitterService
to retrieve the list of tweets.
TwitterTimelineViewModel
contains several properties, which are described in the following list:
• TimelineItems—An IEnumerable
of TimelineItem
objects, which is populated using the ITwitterService
. This list is bound to a ListBox
control in the view.
• ScreenName—The Twitter screen name that was supplied by the user on the sign-in view.
• ImageUrl—The URL to the Twitter user’s avatar. This is retrieved from the Twitter web API.
• Message—An arbitrary string used to supply feedback to the user during retrieval.
• Busy—A bool
value indicating whether an asynchronous operation is under way.
The viewmodel also includes a command of type DelegateCommand<string>
named loadUserTimelineCommand
, which coordinates the retrieval of the list of status updates. LoadUserTimelineCommand
receives a Twitter screen name, which is passed to the ITwitterService
. See the following excerpt:
public class TwitterTimelineViewModel : ViewModelBase
{
readonly ITwitterService twitterService;
public TwitterTimelineViewModel(ITwitterService twitterService)
{
this.twitterService = ArgumentValidator.AssertNotNull(
twitterService, "twitterService");
loadUserTimelineCommand = new DelegateCommand<string>(LoadTimeline);
}
...
}
When the LoadUserTimelineCommand
executes, the LoadTimeline
method is called. This method first ensures that a Twitter screen name has been supplied; if not, it navigates back to the sign-in page.
The ITwitterService.GetCachedTimeline
method executes asynchronously and attempts to retrieve the cached timeline from the local database. When it returns, the viewmodel is populated using the HandleGetTimelineResult
. After attempting to restore the cached timeline, the ITwitterService.GetTimeline
method is used to retrieve the latest data from the Twitter web API. See the following excerpt:
void LoadTimeline(string twitterScreenName)
{
if (string.IsNullOrWhiteSpace(twitterScreenName))
{
var messageService = Dependency.Resolve<IMessageService>();
messageService.ShowError("Please provide a twitter screen name.");
var navigationService = Dependency.Resolve<INavigationService>();
navigationService.GoBack();
return;
}
if (twitterScreenName == screenName)
{
return;
}
Busy = true;
try
{
Message = "Retrieving cached timeline from database.";
twitterService.GetCachedTimeline(twitterScreenName,
args =>
{
HandleGetTimelineResult(args);
Message = "Retrieving real data from Twitter.";
Busy = true;
twitterService.GetTimeline(
twitterScreenName, HandleGetTimelineResult);
});
}
catch (Exception ex)
{
Busy = false;
Message = string.Format("Error retrieving items.");
Console.WriteLine(ex);
}
}
The HandleGetTimelineResult
method is called twice, once for the cached items and then for the live Twitter data. The Error
property of the ResultEventArgs
is used to determine whether the call completed successfully, and if so, the viewmodel’s properties are populated accordingly:
void HandleGetTimelineResult(ResultEventArgs<TwitterUser> e)
{
Busy = false;
if (e.Error != null)
{
Message = "Unable to retrieve timeline.";
return;
}
if (e.Result == null)
{
Message = "No result";
return;
}
Message = "Received result.";
TimelineItems = e.Result.TimelineItems.ToList();
ScreenName = e.Result.ScreenName;
ImageUrl = e.Result.ImageUrl;
}
When the viewmodel is instantiated, in the view’s code-beside class, it is passed a TwitterService
object, as shown:
public TwitterTimelineView()
{
InitializeComponent();
DataContext = new TwitterTimelineViewModel(new TwitterService());
}
The view executes the LoadUserTimelineCommand
in its OnNavigatedTo
handler, as shown:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
string twitterName;
if (NavigationContext.QueryString.TryGetValue("name", out twitterName))
{
ViewModel.LoadUserTimelineCommand.Execute(twitterName);
}
}
The view XAML contains controls to display the Twitter timeline information, as well as the Twitter user information. A ListBox
presents each TimelineItem
using a DataTemplate
, as shown in the following excerpt:
<Grid x:Name="ContentPanel" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Margin="{StaticResource PhoneMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Source="{Binding ImageUrl}"
MaxWidth="60" MaxHeight="60" />
<TextBlock Grid.Column="1" Text="{Binding ScreenName}"
Style="{StaticResource PhoneTextExtraLargeStyle}" />
</Grid>
<ListBox Grid.Row="1"
ItemsSource="{Binding TimelineItems}"
Margin="{StaticResource PhoneHorizontalMargin}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border CornerRadius="10"
BorderBrush="{StaticResource PhoneBorderBrush}"
BorderThickness="2"
Margin="14,5,4,10">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Text="{Binding ReceivedTime,
StringFormat='0:HH:mm:ss'}"
MaxWidth="100" />
<TextBlock Text="{Binding Text}"
Grid.Column="1"
TextWrapping="Wrap"
Style="{StaticResource PhoneTextSmallStyle}" />
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
A ProgressIndicator
is used to monitor the Busy
property of the viewmodel and to display the viewmodel’s Message
text, as shown:
<shell:SystemTray.ProgressIndicator>
<shell:ProgressIndicator IsIndeterminate="{Binding Busy}"
IsVisible="{Binding Busy}"
Text="{Binding Message}" />
</shell:SystemTray.ProgressIndicator>
When a result is received from the Twitter service it is displayed in the view as shown in Figure 26.5.
Visually exploring your database schema can help to reveal inadequacies in its structure. Missing indexes and foreign key relationships can be overlooked when relying solely on metadata in your code. In addition, having the ability to arbitrarily query your tables to see what is in your database can be useful during development.
Given that SQL CE database files usually reside in isolated storage, however, makes exploring the schema difficult.
This section looks at tools to retrieve your local database file from isolated storage and at using Visual Studio and SQL Server Management Studio to view the database on your desktop.
The Windows Phone 7.5 SDK comes with a tool that allows you to read and write to your app’s isolated storage area via the command line. The first part of this section examines the Isolated Storage Explorer present in the Windows Phone SDK. The second part examines a free alternative third-party explorer named WP7 Isolated Storage Explorer.
Depending on your operating system, Isolated Storage Explorer is installed in one of following locations:
• Program FilesMicrosoft SDKsWindows Phonev7.1ToolsIsolatedStorageExplorerTool
• Program Files (x86)Microsoft SDKsWindows Phonev7.1ToolsIsolatedStorageExplorerTool
When using Isolated Storage Explorer, your app must be deployed to the device or emulator. The emulator or device must be running, but the app does not have to be running.
Isolated Storage Explorer uses the following syntax:
ISETool.exe <ts|rs|dir[:device-folder]> <xd|de> <Product GUID> [<desktop-path>]
Table 26.2 lists the command-line options for Isolated Storage Explorer.
To use the Isolated Storage Explorer, open a command window and navigate to the location of ISETool.exe. Retrieve the product GUID from the ProductID attribute of the App element in the project’s WMAppManifest.xml file.
Listing the files and directories at the root directory of an app’s isolated storage area can be done using the following command (replacing the GUID with your app’s product GUID):
ISETool.exe dir xd 11111111-2222-3333-4444-555555555555
To copy all of the files from isolated storage to your computer, use the following command:
ISETool.exe ts xd 11111111-2222-3333-4444-555555555555 "C:Phone"
This command creates a directory on your computer named C:PhoneIsolatedStore and copies the files and directories in isolated storage into the directory.
If the IsolatedStore directory exists and the copy command is performed, the contents of the IsolatedStore directory are overwritten without any warning.
Use the following command to replace all the files in isolated storage with files from your computer:
SETool.exe rs xd 11111111-2222-3333-4444-555555555555 "C:PhoneIsolatedStore"
A useful alternative to the built-in Isolated Storage Explorer is the WP7 Isolated Storage Explorer, which can be found at http://wp7explorer.codeplex.com/. This open source tool can be used to retrieve a local database from isolated storage, and works both on the emulator and a real device. The advantage of the WP7 Isolated Storage Explorer is that it comes with a GUI.
The tool can be used either as a standalone application or within Visual Studio and provides a dockable tool window that displays the contents of your app’s isolated storage directory (see Figure 26.6).
WP7 Isolated Storage Explorer uses WCF service to communicate with your app.
After installing the tool, to use it in your project, add a reference to the IsolatedStorageExplorer
assembly and then add some initialization code to your app’s launching and activated events, as shown:
void Application_Launching(object sender, LaunchingEventArgs e)
{
IsolatedStorageExplorer.Explorer.Start("localhost");
}
void Application_Activated(object sender, ActivatedEventArgs e)
{
IsolatedStorageExplorer.Explorer.RestoreFromTombstone();
}
The ability to retrieve files from isolated storage and open them on your desktop is very useful, and being able to retrieve a local database from isolated storage means that it can then be loaded into Visual Studio or SQL Server Management Studio, allowing you to explore the schema and to verify the results of your mapping attributes. It is also possible to modify the structure of a database and use the SqlMetal tool to generate your data model, including its DataContext
and entity classes. The database-first approach using SqlMetal is discussed in a later section.
Once an SQL CE file has been retrieved from isolated storage, it can be opened in an application such as Visual Studio or Microsoft SQL Server Management Studio.
To open an .sdf file in Visual Studio, follow these steps:
1. Within Visual Studio, right-click on the Data Connections node in the Server Explorer and select Add Connection. The Add Connection dialog is displayed (see Figure 26.7).
2. Within the Add Connection dialog, set the Data Source to Microsoft SQL Server Compact 3.5.
3. Browse and locate the .sdf file using the Connection Properties section.
4. Select OK.
The Server Explorer pane allows you to explore and modify the database schema (see Figure 26.8), and to view table data by right-clicking on a table node and selecting Show Table Data.
If you prefer to use a dedicated database management tool, such as Microsoft SQL Server Management Studio, you can. The 2008 version was used for this demonstration, but the 2005 version can also be used if it has at least service pack 2 installed.
To open an .sdf file in SQL Server Management Studio, follow these steps:
1. Within SQL Server Management Studio select File and then Connect Object Explorer (see Figure 26.9).
2. Within the Connect to Server dialog, set the Server Type to SQL Server Compact Edition.
3. Use the Database File combo box to browse to your .sdf file.
4. Select Connect.
Once connected, the database is displayed in the Object Explorer. You can write queries to explore the table data using the New Query button on the toolbar (see Figure 26.10).
The ability to visually explore your database schema is important during development. The next section looks at how to generate an entity model from a database using a database-first approach, allowing you to not only view the schema but to make changes to it also.
So far, this chapter has focused on a code-first approach for generating a local database. To reiterate, code-first is the recommended approach for developing on the phone. The database-first approach, however, can also be used, though it is not fully supported.
SqlMetal is a command-line tool used to generate an entity model from a database. SqlMetal is not fully compatible with Windows Phone, as the code it generates does not compile until some minor alterations are made to it.
SqlMetal generates a DataContext
derived class and entities representing database tables. Associations are inferred from the foreign key relationships in the database.
SqlMetal can be launched via a Visual Studio Command Prompt by selecting Microsoft Visual Studio 2010/Visual Studio Tools/Visual Studio Command Prompt (2010) in the Windows Start menu.
In the following example, the database file has been downloaded from isolated storage, using the WP7 Isolated Storage Explorer, and placed in a directory called C:Unleashed. SqlMetal is provided with the database path, the name code file to output, and the namespace in which to place all generated classes (ignore line breaks):
C:Unleashed>SqlMetal C:UnleashedTwitter.sdf
/code:C:UnleashedTwitterDataModel.cs
/namespace:DanielVaughan.WindowsPhone7Unleashed.Examples
Once the code file has been generated, it can be added to your project. But before it can be compiled, two constructors must be removed. The key to identifying the errant constructors is that they both accept an IDbConnection
and resemble the methods shown in the following excerpt:
public Twitter(System.Data.IDbConnection connection)
: base(connection, mappingSource)
{
OnCreated();
}
public Twitter(
System.Data.IDbConnection connection,
System.Data.Linq.Mapping.MappingSource mappingSource) :
base(connection, mappingSource)
{
OnCreated();
}
An example of a generated code file is located in the downloadable sample code. The file is named TwitterDataModel_Generated_Unused.cs.
As stated earlier in this chapter, a local database is read-only when it is located in a XAP file. If you want to provide a prepopulated database that is writable from your app, the database must be copied by your app, from your XAP file, to isolated storage.
Like any other resource, such as an image file, an .sdf database file may be stored as content within your app’s XAP file. The app then reads the database file as a Stream
of bytes and writes the bytes to isolated storage. Once the database has been copied to isolated storage, it is ready to be used by your app.
To prevent the size of your project’s assembly from being unnecessarily increased by the inclusion of a database file, set the file’s Build Action to Content in the Visual Studio Properties pane. This decreases your app’s startup time because it takes less time for the CLR to load a smaller assembly. It does not, however, affect your project’s XAP file size.
Listing 26.6 shows the custom IsolatedStorageUtility
class, whose CopyApplicationResourceToIsolatedStorage
method locates an application resource and copies it as a stream to a specified location in isolated storage.
public class IsolatedStorageUtility : IIsolatedStorageUtility, ISettingsService
{
public void CopyApplicationResourceToIsolatedStorage(
string inResourceName, string outFilename)
{
ArgumentValidator.AssertNotNull(inResourceName, "inResourceName");
ArgumentValidator.AssertNotNullOrWhiteSpace(outFilename, "outFilename");
Uri uri = new Uri(inResourceName, UriKind.Relative);
string destination = outFilename;
int seperatorIndex = destination.LastIndexOf("/");
if (seperatorIndex == destination.Length - 1)
{
throw new InvalidOperationException(
string.Format("Destination '{0}' should not end with '/'",
destination));
}
string directory = null;
if (seperatorIndex != -1)
{
directory = destination.Substring(0, seperatorIndex);
}
using (Stream resourceStream = Application.GetResourceStream(uri).Stream)
{
using (IsolatedStorageFile isolatedStorageFile
= IsolatedStorageFile.GetUserStoreForApplication())
{
if (!string.IsNullOrWhiteSpace(directory)
&& !isolatedStorageFile.DirectoryExists(directory))
{
isolatedStorageFile.CreateDirectory(directory);
}
using (IsolatedStorageFileStream outStream
= isolatedStorageFile.OpenFile(
destination, FileMode.Create))
{
resourceStream.CopyTo(outStream);
}
}
}
}
...
}
A database file can then be copied from a XAP file to isolated storage in a single step:
IsolatedStorageUtility.CopyApplicationResourceToIsolatedStorage(
"/Subdirectory/XapDatabase.sdf",
"/Subdirectory/IsolatedStorageDatabase.sdf");
This can be seen in action in the unit test class named IsolatedStorageUtilityTests
, located in the downloadable sample code (see Listing 26.7). The CopyDatabaseToIsolatedStorage
test method copies an .sdf file to isolated storage and then verifies that the bytes of both files are equivalent.
The CollectionAssert.AreEquivalent
method compares the members of both collections. If all members match, the assert succeeds. The CollectionAssert
class is a part of the Silverlight Unit Testing Framework, discussed in Chapter 22, “Unit Testing.”
[TestMethod]
public void CopyDatabaseToIsolatedStorage()
{
const string databaseName = "TestDatabase.sdf";
IsolatedStorageUtility utility = new IsolatedStorageUtility();
utility.CopyApplicationResourceToIsolatedStorage(databaseName, databaseName);
Uri uri = new Uri(databaseName, UriKind.Relative);
byte[] resourceBytes;
using (Stream resourceStream = Application.GetResourceStream(uri).Stream)
{
resourceBytes = resourceStream.ReadAllBytes();
}
Assert.IsNotNull(resourceBytes, "Resource bytes in null");
Assert.IsTrue(resourceBytes.Length > 0, "Resource bytes length <= 0.");
using (IsolatedStorageFile isolatedStorageFile
= IsolatedStorageFile.GetUserStoreForApplication())
{
using (IsolatedStorageFileStream outStream
= isolatedStorageFile.OpenFile(databaseName, FileMode.Open))
{
byte[] isolatedStorageBytes = outStream.ReadAllBytes();
Assert.IsNotNull(isolatedStorageBytes);
Assert.IsTrue(isolatedStorageBytes.Length > 0);
CollectionAssert.AreEquivalent(resourceBytes, isolatedStorageBytes);
}
}
}
This section begins with a brief note about its inclusion. I did not intend to show the abstraction of the navigation service. When I began work on the sample for this chapter, however, and I reached the point of needing to navigate from the sign-in page to the timeline page, I felt that it would make for a nice demonstration of a custom testable navigation service.
The custom navigation service abstracts the navigation mechanism provided by the System.Windows.Controls.Frame
component or the System.Windows.Navigation.NavigationService
(available via the NavigationService
property of the Page
class).
A custom INavigationService
interface is used to navigate in a UI technology agnostic manner and allows the class implementation of the interface to be replaced to enable unit testing of viewmodels. INavigationService
contains the following three methods:
• GoBack—Navigates the application to the most recent entry in the back navigation history or raises an exception if no entry exists.
• Navigate(Uri source)—Navigates to the content specified by the uniform resource identifier (URI).
• Navigate(string relativeUrl)—Navigates to the content specified by the relative URL.
Included in the downloadable sample code are two implementations of the INavigationService
, which are also present and maintained in the open-source project Calcium (http://calciumsdk.net). The first implementation, named NavigationServiceWrapper
, leverages the System.Windows.Navigation.NavigationService
of a Page
. The second implementation, named FrameNavigationService
, relies on the phone app’s root Frame
(see Listing 26.8). The FrameNavigationService
is the implementation used in the sample for this chapter and is the recommended implementation for use on the phone.
public class FrameNavigationService : INavigationService
{
Frame frameUseProperty;
Frame Frame
{
get
{
if (frameUseProperty == null)
{
frameUseProperty = (Frame)Application.Current.RootVisual;
}
return frameUseProperty;
}
}
public FrameNavigationService(Frame frame)
{
frameUseProperty = ArgumentValidator.AssertNotNull(frame, "frame");
}
[InjectDependencies]
public FrameNavigationService()
{
/* Intentionally left blank. */
}
public void GoBack()
{
Frame.GoBack();
}
public void Navigate(Uri source)
{
ArgumentValidator.AssertNotNull(source, "source");
Frame.Navigate(source);
}
public void Navigate(string relativeUrl)
{
ArgumentValidator.AssertNotNull(relativeUrl, "relativeUrl");
Frame.Navigate(new Uri(relativeUrl, UriKind.Relative));
}
}
The INavigationService
is placed in the IoC container in the App
class’s InitializeContainer
method, as shown:
void InitializeContainer()
{
SimpleContainer container = new SimpleContainer();
container.InitializeServiceLocator();
...
Dependency.Register<INavigationService, FrameNavigationService>(true);
}
The InjectDependencies
attribute on the FrameNavigationService
parameterless constructor, indicates to the dependency injection system that it should use that particular constructor when instantiating the class.
In the Twitter timeline viewer sample for this chapter, the TwitterSignInViewModel
class uses the INavigationService
to navigate to the TimelineView
page. TwitterSignInViewModel
relies on a UriMapping
to navigate to the page. The URI mapping is located in the App.xaml file of the project and causes the root frame of the app to direct to TwitterTimelineView.xaml
whenever an attempt is made to navigate to a URI beginning with /TwitterTimeline/. See the following excerpt from the App.xaml file:
<phone:PhoneApplicationFrame.UriMapper>
<navigation:UriMapper>
<navigation:UriMapper.UriMappings>
<navigation:UriMapping Uri="/TwitterTimeline/{name}"
MappedUri="/DataDrivenApps/TwitterExample
/TwitterTimelineView.xaml?name={name}" />
</navigation:UriMapper.UriMappings>
</navigation:UriMapper>
</phone:PhoneApplicationFrame.UriMapper>
The screen name of the Twitter user is passed using a query string parameter called name.
By abstracting the INavigationService
, you increase the reusability of your viewmodel code, and it enables you to coordinate navigation via viewmodel logic. This alleviates the logic fragmentation that can occur when relying on the view to perform navigation.
LINQ to SQL allows you to capture the generated SQL as it is being sent to a local database. This can be achieved by creating a custom TextWriter
that outputs to the Visual Studio console window using Debug.WriteLine
. An instance of the custom TextWriter
is assigned to the Log
property of the DataContext
class.
John Gallardo demonstrates how to do this on his blog at http://bit.ly/mJYY74. The custom TextWriter
, called DebugStreamWriter
, is shown in Listing 26.9.
public class DebugStreamWriter : TextWriter
{
readonly int bufferSize = 256;
readonly StringBuilder stringBuilder;
public DebugStreamWriter(int bufferSize = 256)
{
this.bufferSize
= ArgumentValidator.AssertGreaterThan(0, bufferSize, "bufferSize");
stringBuilder = new StringBuilder(bufferSize);
}
public override Encoding Encoding
{
get
{
return Encoding.UTF8;
}
}
public override void Write(char value)
{
stringBuilder.Append(value);
if (stringBuilder.Length >= bufferSize)
{
Flush();
}
}
public override void WriteLine(string value)
{
Flush();
using (var reader = new StringReader(value))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Debug.WriteLine(line);
}
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Flush();
}
}
public override void Flush()
{
if (stringBuilder.Length > 0)
{
Debug.WriteLine(stringBuilder);
stringBuilder.Clear();
}
}
}
To send all LINQ to SQL logging output to the DebugStreamWriter
, it is assigned to the Log
property of the DataContext
class. I have chosen to do this in the TwitterDataContext
class, as shown:
public class TwitterDataContext : DataContext
{
public TwitterDataContext(string connection) : base(connection)
{
Log = new DebugStreamWriter();
}
...
}
With this in place, we are able to view the activity of the DataContext
via the Visual Studio Output pane (see Figure 26.11).
Observing the generated SQL can help you gain an understanding of how LINQ to SQL works, as well as giving you the opportunity to optimize query complexity.
It is almost inevitable that when updating your app, adding new features and so forth, you will want to make changes to your database schema. Fortunately, local database support on the phone includes the ability to perform additive changes to a database, such as adding tables, columns, indexes, or associations, while preserving the data in the database.
For more complex schema changes, however, such as dropping a column from a table, you need to create a new database and migrate the data to the new schema.
Changes to your local database schema begin with changing the object model of the corresponding data context. Therefore, modify your entity classes first to reflect the desired state of the schema.
The Microsoft.Phone.Data.Linq.DatabaseSchemaUpdater
class provides four schema altering methods:
• AddColumn<T>(
string
columnPropertyName)
• AddTable<T>()
• AddIndex<T>(
string
indexName)
• AddAssociation<T>(
string
associationPropertyName)
The following sections discuss each of these methods in greater detail.
AddColumn
allows you to add a new column to an existing table. For example, if you added a new property to the TwitterUser
class to record the user’s age, you would then need to use the DatabaseSchemaUpdater
to create a new column to store the Age
value. This could be achieved in the following manner:
using (DataContext dataContext
= new TwitterDataContext("isostore:Twitter.sdf"))
{
DatabaseSchemaUpdater updater
= dataContext.CreateDatabaseSchemaUpdater();
updater.AddColumn<TwitterUser>("Age");
updater.Execute();
}
The AddColumn
method only allows columns to be added that can contain null values. Therefore, decorating a property with [Column(CanBeNull = false)]
prevents the AddColumn
method from succeeding and causes an SqlCeException
to be raised when the DatabaseSchemaUpdater.Execute
method is called.
The database does not change to reflect updates from the DatabaseSchemaUpdater
until its Execute
method is called, at which point all changes are submitted to the local database as a single transaction, which ensures the database maintains integrity if it is interrupted, such as when the user exits the app during an upgrade.
Using the AddTable
method allows you to create a new table for an entity class. On adding a new table, the DatabaseSchemaUpdater
also applies the metadata associated with the entity class and creates any related indexes or foreign key constraints.
To add a new entity to the Twitter data model, we first create that entity and decorate it with Table
and Column
attributes. In the following example, the TwitterFriend
class represents a Twitter user who happens to be a contact of another Twitter user (see Listing 26.10). TwitterFriend
contains an Id
and a ScreenName
property. In addition, an index is declared for the ScreenName
property.
[Table]
[Index(Columns = "ScreenName")]
public class TwitterFriend : NotifyPropertyChangeBase
{
string id;
[Column(IsPrimaryKey = true)]
public string Id
{
get
{
return id;
}
set
{
Assign("Id", ref id, value);
}
}
string screenName;
[Column]
public string ScreenName
{
get
{
return screenName;
}
set
{
Assign("ScreenName", ref screenName, value);
}
}
}
The following code is used to upgrade the schema to include a new table for storing TwitterFriend
data:
using (DataContext dataContext
= new TwitterDataContext("isostore:Twitter.sdf"))
{
DatabaseSchemaUpdater updater
= dataContext.CreateDatabaseSchemaUpdater();
updater.AddTable<TwitterFriend>();
updater.Execute();
}
It can be seen from Figure 26.12 that after execution, the Twitter database contains the new table, with a secondary index on the ScreenName
column, as desired.
Suboptimal query performance is sometimes caused by table scans, where a query causes every row of a table to be examined. Depending on the kind of data in a column, bottlenecks of this nature can often be rectified by placing an index on the column.
The need for an index may be discovered after a table has been populated with a substantial number of records and after an app has been published to the marketplace, or the need may arise with the introduction of a new feature that causes the table to be queried in a different way. In both cases, adding an index to an existing table is done by decorating the entity class with an Index
attribute and then by calling DatabaseSchemaUpdater.AddIndex
.
For example, if we add an EmailAddress
property to the TwitterFriend
class and later decide to add an index to improve query performance, we would first decorate the TwitterFriend
class with an index attribute like so:
[Index(Name = "EmailAddressIndex",
Columns = "EmailAddress", IsUnique = true)]
public class TwitterFriend : NotifyPropertyChangeBase
{
...
}
Once decorated with the Index
attribute, the schema can be updated to include a new index for the column, as shown in the following excerpt:
using (DataContext dataContext
= new TwitterDataContext("isostore:Twitter.sdf"))
{
DatabaseSchemaUpdater updater
= dataContext.CreateDatabaseSchemaUpdater();
updater.AddIndex<TwitterFriend>("EmailAddressIndex");
updater.Execute();
}
AddAssociation
allows you to add a one-to-many association between two entities. This method results in a foreign key constraint being added to the column that holds the primary key of the foreign table.
AddAssociation
accepts a single parameter: associationPropertyName
, which is the name of the entity member that holds the foreign key of another table.
To illustrate, if we were to add a new association between the TwitterUser
and the TimelineItem
classes that matches the existing association, we would first set about by adding a new property to the TimelineItem
class to hold the id of a TwitterUser
like so:
[Table]
public class TimelineItem : NotifyPropertyChangeBase
{
string twitterUser2Id;
[Column]
public string TwitterUser2Id
{
get
{
return twitterUser2Id;
}
set
{
Assign("TwitterUser2Id", ref twitterUser2Id, value);
}
}
EntityRef<TwitterUser> user2;
[Association(Storage = "user2", ThisKey = "TwitterUser2Id",
IsForeignKey = true, Name = "FK_TwitterUser2")]
public TwitterUser TwitterUser2
{
get
{
return user2.Entity;
}
set
{
user2.Entity = value;
}
}
...
}
Explicitly providing a name for the foreign key constraint avoids collisions with existing constraint names, especially if similar associations have already been defined, as is the case with our TwitterUser
property.
On the other side, the one side of the association, we would create a new EntitySet
property for the new TimelineItem
children, as shown:
[Table]
[Index(Columns = "ScreenName")]
public class TwitterUser : NotifyPropertyChangeBase
{
readonly EntitySet<TimelineItem> timelineItems2;
[Association(
Storage = "timelineItems2",
OtherKey = "TwitterUserId")]
public EntitySet<TimelineItem> TimelineItems2
{
get
{
return timelineItems2;
}
set
{
timelineItems2.Assign(value);
}
}
...
}
To materialize the association in the database, we would then perform the following updates:
using (TwitterDataContext dataContext
= twitterDatabaseUtility.CreateContext())
{
DatabaseSchemaUpdater updater
= dataContext.CreateDatabaseSchemaUpdater();
updater.AddColumn<TimelineItem>("TwitterUser2Id");
updater.AddAssociation<TimelineItem>("TwitterUser2");
updater.Execute();
}
The DatabaseSchemaUpdater
class provides a DatabaseSchemaVersion
property that allows you to programmatically distinguish between different versions of your database.
When a local database is created with the DataContext.CreateDatabase
method, a table named _Version is automatically created. This table holds the current DatabaseSchemaVersion
number.
In the following example, the database version is used to determine what needs to be done to update the schema to the current version:
using (TwitterDataContext dataContext
= twitterDatabaseUtility.CreateContext())
{
DatabaseSchemaUpdater updater
= dataContext.CreateDatabaseSchemaUpdater();
int databaseVersion = updater.DatabaseSchemaVersion;
if (databaseVersion < 2)
{
updater.AddColumn<TwitterUser>("Homepage");
updater.DatabaseSchemaVersion = 2;
updater.Execute();
}
if (databaseVersion < 3)
{
updater.AddColumn<TwitterUser>("Birthday");
updater.DatabaseSchemaVersion = 3;
updater.Execute();
}
}
As you make changes to your schema, take note of them. This eases the burden of writing schema versioning code later.
So far in this chapter, you have seen how LINQ to SQL has been used to map entity classes to database tables in a one-to-one fashion. There are times, however, when constraining an entity model to a flat hierarchy can feel too restrictive, and you may want to use inheritance in your entity model. This is possible with LINQ to SQL using single table inheritance.
When multiple classes of a hierarchy are stored within the same table, this is called single table inheritance. It works by dedicating a column within the table to represent the type of the particular entity class.
While this approach decreases the level of normalization in a database, it can be an effective approach for entity classes that have a small degree of variation in their storage needs.
The following example creates an entity model representing bank accounts. These bank account objects are stored using a single table in a local database.
The example code for this section is located in the /DataDrivenApps /LinqToSqlInheritanceMapping directory of the WindowsPhone7Unleashed.Examples project in the downloadable sample code.
At the base of the class hierarchy is the BankAccount
class, and deriving from this are the three classes: SavingsAccount
, CheckingAccount
, and FixedDepositAccount
(see Figure 26.13).
The two primary elements for mapping an inheritance hierarchy are the InheritanceMapping
attribute and the Column
attribute’s IsDiscriminator
property.
Listing 26.11 shows that the BankAccount
class declares each of its derived types using the InheritanceMapping
attribute. The Code
property of the InheritanceMapping
attribute is the unique column value representing the derived type. The BankAccountTypeId
is decorated with a Column
attribute that has its IsDescriminator
property set to true, indicating that it corresponds to the InheritanceMapping
attribute’s Code
property.
Thus, if an entity is a SavingsAccount
, then LINQ to SQL sets its BankAccountTypeId
to 0. If the entity is a CheckingAccount
, then BankAccountTypeId
is set to 1. And so on.
[Table]
[InheritanceMapping(Code = 0, Type = typeof(SavingsAccount), IsDefault = true)]
[InheritanceMapping(Code = 1, Type = typeof(CheckingAccount))]
[InheritanceMapping(Code = 2, Type = typeof(FixedDepositAccount))]
public class BankAccount : NotifyPropertyChangeBase
{
int id;
[Column(
IsPrimaryKey = true,
DbType = "INT IDENTITY NOT NULL",
IsDbGenerated = true,
UpdateCheck = UpdateCheck.Never)]
public int Id
{
get
{
return id;
}
set
{
Assign("Id", ref id, value);
}
}
int bankAccountTypeId;
[Column(DbType = "Int NOT NULL", IsDiscriminator = true)]
public int BankAccountTypeId
{
get
{
return bankAccountTypeId;
}
set
{
Assign("BankAccountTypeId", ref bankAccountTypeId, value);
}
}
decimal balance;
[Column]
public decimal Balance
{
get
{
return balance;
}
set
{
Assign("Balance", ref balance, value);
}
}
}
Only the base class is decorated with a Table
attribute; entity subclasses are not. Members of the subclass that are to be stored in the database are decorated with a Column
attribute (see Listing 26.12).
[Index(Columns = "CheckbookCode")]
public class CheckingAccount : BankAccount
{
string checkbookCode;
[Column(DbType = "NCHAR(16)")]
public string CheckbookCode
{
get
{
return checkbookCode;
}
set
{
Assign("CheckbookCode", ref checkbookCode, value);
}
}
}
The custom BankingDataContext
class has a single BankAccounts
property and is used in the same manner as the Twitter example earlier in this chapter (see Listing 26.13).
public class BankingDataContext : DataContext
{
public BankingDataContext(string connection) : base(connection)
{
Log = new DebugStreamWriter();
}
public Table<BankAccount> BankAccounts
{
get
{
return GetTable<BankAccount>();
}
}
}
The BankingDataContextTests
class, in the WindowsPhone7Unleashed.Tests project, demonstrates storage and retrieval of various banking entity types. A custom BankingDatabaseUtility
is used to initialize the database file. Two accounts are inserted into the database and then retrieved using LINQ to SQL. See the following excerpt:
[TestMethod]
[Tag("i1")]
public void ContextShouldReadAndWrite()
{
using (BankingDataContext context = databaseUtility.CreateContext())
{
SavingsAccount savingsAccount = new SavingsAccount {Balance = 50};
CheckingAccount checkingAccount = new CheckingAccount
{
Balance = 100,
CheckbookCode = "12345"
};
context.BankAccounts.InsertAllOnSubmit(new List<BankAccount>
{
savingsAccount,
checkingAccount
});
context.SubmitChanges();
}
using (BankingDataContext context = databaseUtility.CreateContext())
{
List<BankAccount> accounts = context.BankAccounts.ToList();
Assert.IsTrue(accounts.Count > 2, "There should be two accounts.");
IQueryable<SavingsAccount> savingsAccounts
= context.BankAccounts.OfType<SavingsAccount>();
Assert.IsTrue(savingsAccounts.Any());
SavingsAccount account = savingsAccounts.First();
Assert.IsTrue(account.Balance == 50, "Balance should be 50");
}
}
Having support for mapping inheritance hierarchies in LINQ to SQL is a tolerable way of bridging the object relational divide.
LINQ to SQL has built-in support for optimistic concurrency; an entity is able to be retrieved and used in more than one operation at a time. When an entity is retrieved from the database, and then updated, if changes by another party occur in the interim, they are identified and a conflict is detected.
The mechanism supporting this feature is configured using two Column
attribute properties: IsVersion
and UpdateCheck
.
IsVersion
allows you to designate an entity class member to be used for optimistic concurrency control (OCC). Before committing a change to an entity, the data context verifies that no other transaction has modified its data. If the check reveals conflicting modifications, the committing transaction rolls back and an exception is raised.
IsVersion
is not required for conflict detection. When not specified, however, the data context must retain copies of the entity’s member values and must compare the original values with the database values to detect conflicts, which is not terribly efficient.
LINQ to SQL supports multitier applications by allowing entities to be attached to a DataContext
. Such an entity may have been retrieved using a different DataContext
instance or deserialized after being sent over the wire from another tier via a web service.
In the following example you see that by adding a version property to the BankAccount
class (presented in the previous section), you can prevent conflicting changes from being written to the database. BankAccount
now contains a DateVersion
property of type byte[]
:
byte[] dataVersion;
[Column(
IsVersion = true,
IsDbGenerated = true,
UpdateCheck = UpdateCheck.Never)]
public byte[] DataVersion
{
get
{
return dataVersion;
}
set
{
Assign("DataVersion", ref dataVersion, value);
}
}
The DataVersion
value is materialized as a ROWVERSION field in the database (see Figure 26.14). The ROWVERSION data type causes the table field to be automatically updated whenever a row update occurs.
The BankingDataContextTests
class contains a test method for demonstrating conflict detection. It creates a CheckingAccount
, called beforeAccount
, and inserts it into the database.
It then retrieves the same account, this time in the scope of a new BankingDataContext
, sets its CheckbookCode
to a new value, and then updates the database.
Finally, it attaches the beforeAccount
instance to a new DataContext
and attempts to update the CheckbookCode
to a new value. See the following excerpt:
[TestMethod]
[Tag("i2")]
[ExpectedException(typeof(ChangeConflictException))]
public void ContextShouldEnforceUpdateChecking()
{
CheckingAccount beforeAccount;
using (BankingDataContext context = databaseUtility.CreateContext())
{
beforeAccount = new CheckingAccount
{
Balance = 100,
CheckbookCode = "11111"
};
context.BankAccounts.InsertOnSubmit(beforeAccount);
context.SubmitChanges();
}
using (BankingDataContext context = databaseUtility.CreateContext())
{
CheckingAccount afterAccount
= (CheckingAccount)context.BankAccounts.Where(
x => x.Id == beforeAccount.Id).First();
afterAccount.CheckbookCode = "22222";
context.SubmitChanges();
}
using (BankingDataContext context = databaseUtility.CreateContext())
{
context.BankAccounts.Attach(beforeAccount);
beforeAccount.CheckbookCode = "33333";
context.SubmitChanges();
}
}
When context.SubmitChanges
is called, a conflict is detected because the afterAccount
object, representing the same account, was updated in the database in the interim. This raises a ChangeConflictException
(see Figure 26.15), which is defined as an expected exception, using the ExpectedException
test attribute shown in the previous excerpt.
Providing a dedicated version property within your entity classes is good practice when enabling conflict detection. When not used, the data context relies solely on the Column
attribute’s UpdateCheck
property for inferring conflict detection behavior.
UpdateCheck
informs the data context when to verify that a member value in the database has not changed since it was retrieved. The following is the list of valid UpdateCheck
values:
• Always
• Never
• WhenChanged
When performing an update without the existence of a dedicated version member, LINQ to SQL generates an UPDATE statement with a WHERE clause that includes the original column values. If the values do not match those in the database, no update occurs, and a conflict is detected, raising an exception.
The default value of UpdateCheck
is UpdateCheck.Always
. If set to UpdateCheck.Always
, conflict detection occurs even if the entity’s member value has not changed.
If the UpdateCheck
attribute property is set to UpdateCheck.Never
, the member does not participate in conflict detection. You can use this value to effectively switch off conflict detection for a particular member.
If the UpdateCheck
attribute property is set to UpdateCheck.WhenChanged
, conflict detection for the member occurs only if the value has changed in the entity object.
This chapter began with an overview of how local databases are deployed to Windows Phone.
The chapter explored the code-first approach to data model creation and walked through a sample Twitter timeline viewer that caches data in a local database, during which you explored several facets of LINQ to SQL including entity creation, change tracking, and entity multiplicity.
The chapter then looked at the database-first approach to data model creation. You saw how to retrieve a local database file from isolated storage, and how to use the SqlMetal tool to generate a data model from a database.
The chapter then examined a custom navigation service that allows navigation from a viewmodel without reliance on the built-in navigation of the phone, which can increase the testability of viewmodel classes.
A technique for observing LINQ to SQL queries using a custom log was discussed. You saw how to upgrade a schema without losing data, and how to use version numbering to provide incremental updates to your database schema.
Finally, the chapter looked at mapping inheritance hierarchies using LINQ to SQL, and at concurrency and conflict detection.
3.15.34.154