A Developer's Perspective on WinFS: Part 2

Shawn Wildermuth
https://adoguy.com

July 2004

Applies to:
   Longhorn Community Technical Preview, WinHEC 2004 Build (Build 4074)

Summary: WinFS makes searching and manipulating WinFS objects very straightforward. Learn how to specify a very simple syntax for most cases, but also how the WinFS API supports a rich search syntax to perform complicated searches. (13 printed pages)

UPDATE: In spite of what may be stated in this content, "WinFS" is not a feature that will come with the Longhorn Operating System. However, "WinFS" will be available on the Windows platform at some future date, which is why this article continues to be provided for your information.

Download the WinFS sample that accompanies this article.

Contents

Since Our Last Episode...
Digging Deeper
Searching
Short Course in OPath
Working with WinFS Objects
Conclusion
For More Information

Some time has elapsed since Microsoft has unveiled WinFS and I expect that you are starting to get comfortable with the key concepts. It is time to put on the batting helmet and dig into the API in real-world situations. In this article, I will show you how to do the common tasks that you will need for your day-to-day use of WinFS.

If you are unfamiliar with WinFS, please see Part 1 of this article series for an introduction to WinFS.

Since Our Last Episode . . .

Since we last visited our heroes (the WinFS dev team), Microsoft has released a new build of Longhorn. The WinHEC Build 4074 includes some significant WinFS changes since the PDC 2003 developer preview build. This was expected. These very early builds of Longhorn are very much a moving target. Whole areas may be completely redesigned before the actual commercial release of the operating system. This is a good thing, actually. Once the programmatic interfaces to Longhorn are released, we are likely to have to live with them for years to come. In this early period of development, we get to provide the teams with feedback to make sure they develop something we really want and it is straightforward to use.

Assemblies

While the namespaces have not changed, the WinFS assemblies have had a reworking. They have separated most of the top level WinFS namespaces into their own assemblies. For example, in our old example, we just needed the following assemblies:

  • System.Storage.dll
  • System.Schemas.dll
  • WinCorLib.dll

This has now changed quite a bit. You will usually need the System.Storage.WinFS.dll assembly in most of your projects. It represents the basic interfaces and classes (for example, Item and ItemContext). In addition, System.Storage.Core.dll is required for most WinFS work. Once you have both of those assemblies, most of the functional groups are separated into separate assemblies. For example, if you are working with Contacts, you will need the System.Storage.Contacts.dll assembly; if you are working with Files, you will need the System.Storage.Files.dll assembly. On some rare occasions, you will also need the WindowBase.dll assembly. This assembly contains much of the core Longhorn-specific content. All the Longhorn assemblies are now located in one place: <WindowsDir>\Microsoft.NET\Windows\v6.0.4030 (depending on the current version of the Longhorn assemblies).

OptionalValue and Nullable

In my first article, I explained how the OptionalValue generic type was used to represent possible values that could be null. This type has been replaced with the Nullable type. In our examples, this works identically like the OptionalValue, but there may be subtle differences between the two implementations.

Relationship Changes

The notion of how relationships are created and handled has not changed in the new build, but the implementation of how to use relationships on most objects has changed. Most WinFS schemas now have properties for "in" and "out" relationships; "in" relationships being a relationship where the Item is the target of the relationship; and "out" relationships being a relationship where the Item is the source of a relationship. In other words, an employee has an "in" relationship between an employer and the employee whereas the employer has an "out" relationship between itself and its employees. Typically, "in" relationships are one-to-one relationships from the perspective of the Item and "out" relationships are one-to-many. To match these concepts, the WinFS Item class supports two different properties to separate these relationships, Item.IncomingRelationships and Item.OutgoingRelationships. Consequently, there are two types of Relationship collections to go with these new relationship perspectives, TargetRelationshipCollection and SourceRelationshipCollection, respectively. It is important to understand that a Relationship may be in either of these collections; there are not two different types of Relationships, just two perspectives, depending on what object you are referring to a relationship from.

For the most part, WinFS simplifies relationships by including typed collections for specific types of relationships. For example, when retrieving an address from a Person we need to navigate an "out" relationship between the Person and their addresses, specifically the OutContactLocationRelationships property:

// Retrieves an Address from a Person Object, if any.
private Address GetAddress(Person person)
{
  // Make sure there are addresses to get.
  if (person.OutContactLocationsRelationships.GetCountFromStore() > 0)
  {
    // Since Indexers are not working in 
    // this build, just grab the first
    // Item using the IEnumerator.
    IEnumerator en = 
                  person.OutContactLocationsRelationships.GetEnumerator();
    en.Reset();
    en.MoveNext();
    ContactLocations contactLocations = en.Current as ContactLocations;
    
    // If we found an address, 
    // it has locations, 
    // and the locations have LocationElements.
    // Return the Address.
    if (contactLocations != null 
      && contactLocations.Locations != null 
      && contactLocations.Locations.LocationElements.Count > 0
      && contactLocations.Locations.LocationElements[0] != null)
    {
      return contactLocations.Locations.LocationElements[0] as Address;
    }
  }
  return null;
}

This does work, but I would expect the final API to include more direct "helper" methods to get at simple types of data for different types of WinFS objects. For example, I would expect the Person class to have an Addresses collection that would return address objects more simply. This code does the job, but it takes quite a bit of spelunking to understand the object model as it stands today. Exacerbating the problem is the fact that Relationship collections' indexers are broken in this build, so you may need to use the IEnumerable interface (as we did in the code example above) to get at specific types of relationships and their related data.

Digging Deeper

In my previous WinFS article I explained how WinFS can potentially change the way you view data in an operating system. In this article I will show you how to do more pedestrian work with WinFS—searching, creating, changing, and deleting WinFS objects. To do this, we are going to put together a simple Windows Forms application for managing Contacts in Longhorn as seen in Figure 1:

Figure 1. Our Windows Forms application for managing Contacts

Searching

Before you can work with WinFS objects, you must first be able to find them. Searching WinFS is fairly straightforward. It uses the OPath syntax that is part of ObjectSpaces. In the most basic case, OPath supports property searches. For example, to do a blanket search through all of WinFS for the first object named Don Box:

// Create a Context object.
using (ItemContext context = ItemContext.Open())
{
  // Perform a search.
  Object found = context.FindOne(typeof(Item), "DisplayName = 'Don Box'");

  // Cast it to an Item.
  Item item = (Item) found;
}

Searching within WinFS begins with the ItemContext class. The ItemContext class supports a FindOne method that will return the first object that satisfies the search. The FindOne method takes a Type of the objects we are searching for and the OPath search string. FindOne returns either the first found object or null if no matches are found.

The search type specified in the FindOne method must be a Type that derives from the System.Storage.Item class. In our example we are searching for any object in the WinFS store that is named "Don Box". We could limit the search by changing the Type. For example, if we wanted to find the Person object named "Don Box", we would change the type of object we searched for:

// Perform a search.
Object obj = context.FindOne(typeof(Person), "DisplayName = 'Don Box'");

OPath is a specialized syntax for dealing with object graphs. OPath is made up of XPath sprinkled liberally with SQL syntax. At its most basic, OPath supports property comparisons. In our example above, I am looking for an object whose DisplayName is 'Don Box' (DisplayName = 'Don Box'). I explore OPath further later in this article.

Often you will want to find all WinFS objects that match a particular criterion. WinFS supports this with the ItemContext.FindAll method. In our example application we search all the People in our Contacts that match a particular OPath query. In order to do this, we can change our search to be:

// Create a Context object.
using (ItemContext context = ItemContext.Open())
{
  // Perform a search.
  FindResult result = context.FindAll(typeof(Person), 
                                      "DisplayName = 'Don Box'");
  ...
}

A couple of things have changed:

  • We are calling the FindAll method to get all the matches to our query.
  • We are expecting a FindResult object instead of the single result we saw above.
  • Lastly, we send the Person type to the method to specify that we really just want People in our Contacts.

Unlike the FindOne method, the FindAll method returns a FindResult object. The FindResult object is a collection of results. It supports the IEnumerable interface, which means you can use a foreach loop to iterate through the results. For example, we use this method to get the Contacts and put them in a tree view:

// Go through each result and add it.
foreach (Person person in result)
{
  // Create a new TreeNode with the name of the person.
  TreeNode node = new TreeNode(person.DisplayName.Value);

  // Store the WinFS object in the node.
  node.Tag = person;

  // Add the Node to the Tree.
  top.Nodes.Add(node);
}

This code takes the results of the search we executed earlier and creates a new TreeNode for each of the people returned in the search. There may be times you want all objects, not just specific ones. In that case you can perform the search, but omit the OPath query:

// Create a Context object.
using (ItemContext context = ItemContext.Open())
{
  // Perform a search.
  FindResult result = context.FindAll(typeof(Person));
  // context.FindAll(typeof(Person), ""); works too.

  ...
}

In addition to these simple ways of performing a search, WinFS supports a Query object that can be used to perform the same sort of searches:

// Using a Query Object
Query query = new Query();
query.Type = typeof(Person);
query.Filter = criterion;
query.Sort = "BirthDate";
FindResult res = _context.FindAll(query);

Instantiating a Query object is as simple as creating a new object. You need to specify a Type and a Filter much like you do when you call FindAll or FindOne. Additionally, the Query object allows you to specify the field that will be used to sort the results. The documentation does not elaborate on how you would do compound or descending sorts. While the Query object is a bit more verbose to set up, it does offer the ability to keep a Query around to allow reuse of a particular query as well as adding support for sorting to queries.

Not surprisingly, performing WinFS searches is very straightforward. The key to getting the most out of searching is learning the query syntax.

Short Course in OPath

OPath is not difficult to learn. Microsoft has attempted to create a simple and powerful search language by merging ideas from XPath and SQL. See "WinFS" OPath Language in the Longhorn SDK for more information. Now let's look at a couple of the most useful uses for OPath.

Property Searches

Simple property queries are just <property name> <operator> <value>. Case is significant on the property name, but not on the value:

DisplayName = 'Don Box'
BirthDate < '01/01/2000'

Wildcard Searches

Wildcard searches uses the LIKE operator (much like in SQL) to do approximate searching. These searches use the % and _ characters to represent wildcards. The % represents one or more characters and the _ represents exactly one character:

DisplayName LIKE 'Don%'
DisplayName LIKE 'D_n B_x'

List Searches

You can search for all elements that match a list of values using the IN syntax (like in SQL). This allows you to specify a list of possible values and match every item that matches one of the items in the list:

DisplayName IN ('Don Box', 'Chris Sells', 'Brent Rector')
BirthDate IN ('05/01/1967','11/02/1960')

In other words, the first search is asking the query to find all items that have a display name of 'Don Box', 'Chris Sells', OR 'Brent Rector'.

Collection Element Searches

Collection elements are embedded collections within WinFS objects. For example, in the Person object there is a list of electronic addresses called EAddresses (e.g., e-mail, phone numbers, and IM addresses). You can find contacts with a particular area code (which may not be their primary one) by using the dot syntax that is much like .NET:

PersonalNames.GivenName = 'Don'
EAddresses.AreaCode = '202'

In addition you can use a bracketed (XPath like) syntax:

PersonalNames[GivenName='Don']
EAddresses[AreaCode='202']

Compound Searches

Most of the time simple searches are not powerful enough. OPath supports creating compound searches by linking criterion with AND, OR, and parentheses:

DisplayName LIKE 'Don%' AND BirthDate < '01/01/2000' 
DisplayName LIKE 'Don%' OR BirthDate < '01/01/2000' // Same as above

DisplayName LIKE 'Don%' && BirthDate < '01/01/2000' 
DisplayName LIKE 'Don%' || BirthDate < '01/01/2000' // Same as above

(DisplayName LIKE 'Don%' AND BirthDate < '01/01/2000') 
  OR DisplayName = 'Don Box'

When dealing with collection element searches you can use the brackets or parentheses to group together parts of the query:

(EAddresses.Domain = 'microsoft.com' AND EAddresses.UserName = 'dbox') 
  OR EAddresses.Domain = yahoo.com'

EAddresses[Domain = 'microsoft.com' AND UserName = 'dbox'] 
  OR EAddresses.Domain = yahoo.com'

Relationship Searches

Within WinFS, objects can be related through a primitive object called a relationship. A relationship is simply a mapping between two Item objects. For the Items that are related, Relationships are either Incoming or Outgoing. Therefore you can search for objects through their specific relationships. For example, to find all the people who work for Microsoft:

// Perform a search.
string query = 
          "OutEmploymentRelationships.Employer.DisplayName = 'Microsoft'";

FindResult result = _context.FindAll(typeof(Person), query);

These searches are simply collection element searches (see above). The difference is that the relationship properties return collections of Relationship objects. So in this case, when we ask for the OutEmploymentRelationships property, we are actually filtering based on the Employment object (which derives from the Relationship class).

We can perform specific searches to find objects that are directly related to other objects. For example, to find the organization for a particular person, you would search through all Organizations looking for relationships that meet your criterion. In this example, we could search for all the organizations that have a specific person as an employee:

// Get the Person from the Tree Control.
Person person = _theTree.SelectedNode.Tag as Person;
if (person == null) return;

// Perform a search
string query = string.Format(
               "InEmploymentRelationships.Employee.DisplayName = '{0}'", 
                                                      person.DisplayName);
object obj = _context.FindOne(typeof(Organization), query);

// Cast it to our Organization.
Organization myOrg = org as Organization;

Of course, this type of search is not necessary since we could search through the person's InEmploymentRelationships property, but it does show that relationships are handled much like collection element searches.

Performing WinFS searches is usually only half the job. Once we have found some WinFS objects, we will want to change them.

Working with WinFS Objects

Now that we have some WinFS objects to work with, we can manipulate them as needed. Our example application supports creation, deletion, and changing of contacts. Let's see the most common tasks that you will need to do on WinFS objects.

Creating New WinFS Objects

As we covered in the first article, WinFS objects cannot exist without a holding relationship to other objects (or a root object) in a WinFS store. So when creating new objects, we have to find a home for them.

Folder objects are used to store WinFS objects. As we saw in the last article, we could create a folder and add a file:

// Create a folder.
Folder folder = new Folder(root, "FirstFolder");

// Create a new file in the first folder.
File file = new File(one, "test.txt");

// Save everything.
_context.Update();

Since our example includes Contacts, we want to find the specialized folder that normally holds contacts for a specific user. This folder can hold any type of item, but is normally used to specify a user's collection of contacts. Before we can create new contacts (and new Organizations), we must have a way of getting to the Contact Folder. Fortunately, WinFS has a class for common system-level folders like the Contact folder, the WellKnownFolder class. The WellKnownFolder for the current user's contact folder can be found by calling the UserDataFolder.FindMyPersonalContactsFolder method:

// Get the contact folder for the user.
WellKnownFolder contactFolder = 
  UserDataFolder.FindMyPersonalContactsFolder(_context);

Before we can add the person to the folder, we must create the person. Creating a WinFS object can be done with their empty constructor:

// Create a new Person.
Person person = new Person();

We need to set the members of the new object before we can add it to the folder:

// Set the person's Name.
person.DisplayName = "Bob Smith";

Adding an item to a folder is a matter of creating a new relationship between the two objects. The most straightforward way to do this is to add an item to the OutFolderMemberRelationships property:

contactFolder.OutFolderMemberRelationships.AddItem(person, 
                                               Guid.NewGuid().ToString());

When we call the AddItem method, we need to supply both the object that will be the target for the relationship, and also a name for the relationship. Because the relationships must be uniquely named, I am using a GUID (globally unique identifier) to name my relationship. GUIDs are simply very large numbers (128-bit) that are generated by the Guid structure. By using the NewGuid method, I am generating a new Guid structure that is guaranteed to be unique and converting it to a string.

Now that we have a new object, we will want to save it.

Modifying WinFS Items

Changing the data that WinFS objects hold is not difficult. As we saw in the first article, we can set properties just like any other .NET types:

// Save the Display Name.
person.DisplayName = contactTextBox.Text;

This code just assigns the text box's string to the DisplayName property. DisplayName is a simple scalar type on all WinFS Items so we can just treat it like a property. In addition, when you are updating properties you do not need to deal with the fact that they are Nullable objects. Since you are setting a value to the Nullable object, you will end up with an object that is certainly not null so you can treat it like the expected type. This is the same for values that are not strings:

// Save the BirthDate.
person.BirthDate = DateTime.Parse(birthDateTextBox.Text);

This pattern of setting values works equally well for nested types:

// Save the First Name.
person.PrimaryName.GivenName = contactTextBox.Text;

Nested collections are almost as simple:

// Create a new Phone Number. 
TelephoneNumber phone = new TelephoneNumber();

// Set the phone number.
phone.SetFromUserInputString(phoneTextBox.Text);

// Add it to the person's EAddresses collection.
person.EAddresses.Add(phone);

WinFS wants us to work with individual objects and collections like standard .NET objects and collections. This code should look very pedestrian in that it is just adding an object to a collection and setting properties (or scalar types in WinFS-speak). Once we have made these changes, we want to propagate those changes into the WinFS data store.

Saving Changes

WinFS does not support atomic save operations as you might expect. The problem is that most WinFS objects are related to one other. Saving an object atomically may require several other objects to also be saved. To mitigate this confusion, there is no Save function on Items in WinFS. Instead you would call Update on the ItemContext to save all the changes within that context:

// Save the changed Data.
_context.Update();

All objects in the current context will be saved when the Update method is called. As we saw above, we can add or change an address for a person:

// Create a new Address if one doesn't exist.
Address address = GetAddress(person);

// If it doesn't exist, create it.
if (address == null)
{
  address = new Address();
  Location location = new Location();
  location.LocationElements.Add(address);
  person.OutContactLocationsRelationships.AddLocations(location, 
                                               Guid.NewGuid().ToString());
}

// Save the Address. 
address.AddressLine = _addressTextBox.Text;
address.PrimaryCity = _cityTextBox.Text;
address.CountryRegion = _stateTextBox.Text;
address.PostalCode = _zipTextBox.Text;

// Save the person.
_context.Update();

When we save the person that this address is attached to, this address will also be saved. This is because all the objects that we created or edited (or if we had deleted any) are in our ItemContext. When you tell WinFS to update the ItemContext, it saves all objects that have changes.

What this ItemContext.Update() API implies is that batched updates are better than lots of smaller updates. In fact, in the PDC 2003 build of Longhorn all WinFS objects had a Save method. I think the idea of removing that call is to get users to think of the overhead of saving objects to the store and encourage batch updates.

Deleting WinFS Items

Sometimes the changes are not just adding data, but removing it as well. You might expect that deleting WinFS objects would be as simple as calling a method to mark it as deleted. Unfortunately, the only way to delete items in this build is to remove them from all holding relationships. For example, to remove a person (and all of its related data), we must remove it from the folder where it resides by calling the RemoveItem method of the Folder.OutFolderMemberRelationships property:

WellKnownFolder contactFolder = 
  UserDataFolder.FindMyPersonalContactsFolder(_context);

contactFolder.OutFolderMemberRelationships.RemoveItem(person);

// Save it.
_context.Update();

Deleting parts of a WinFS object is similar to deleting whole WinFS Items. For example, if we were to delete an individual phone number on a Person we would remove it from the Collection:

TelephoneNumber phone = person.EAddresses[0] as TelephoneNumber;

// Delete the Address.
person.EAddresses.Remove(address);

// Update the Context.
_context.Update();

When removing objects from NestedCollections, you must still save the object to make the object become deleted from the WinFS store.

Conclusion

Our little WinFS is growing up. We are just two public builds into the preview of Longhorn and we are seeing the API change dramatically. Much of this is because users that have had a chance to toy with the Longhorn builds have given feedback on the usability of the API. Keep up the good work and keep pressing Microsoft to make the API a better one that we can and will use for many years to come.

WinFS makes searching and manipulating WinFS objects very straightforward. Searching through WinFS allows us to specify a very simple syntax for most cases but supports a rich search syntax to perform complicated searches when needed. Searching by specifying the type of object we are looking for allows us to use the hierarchical nature of the WinFS schema to our benefit when searching. In addition, the WinFS API allows us to deal with WinFS object like any other managed objects when we need to manipulate them. In most cases we can deal with WinFS objects just like any other classes that we work with in our applications. The power here is that the API should be very intuitive for users of WinFS objects. We can create objects, change objects by manipulating properties, delete objects, and save our changes in batches. In my next article in this series, I will delve deeper into how Relationships work and how you will want to use the Notifications infrastructure of WinFS.

For More Information