Fluent LiveFX Resource Scripts
After writing my Resource Script demo post, I’ve been digging deeper into Live Framework Resource Scripts. Along the way I’ve written a helper library to make them easier to work with. My enhancements focus primarily on keeping your scripts strongly typed and enabling a more concise fluent interface syntax. These enhancements let Intellisense help you out quite a bit more, resulting in greater discoverability and productivity.
I must warn you that the following discussion won’t make much sense unless you’re already somewhat familiar with Resource Scripts. I apologize and promise to follow up in future posts with material that’s more suitable as an introduction, using my library of course. :-)
Strongly typed bindings
If you’ve played with Resource Scripts at all, you’ve almost certainly run into Bindings. These creatures consume magic strings such as “EntryUrl”, “CollectionUrl”, “Request.Title”, “Response.SelfLink”, “Response.DataFeedsLink”, and “Response.DataEntriesLink” to name a few. Yuck! To make matters worse, the types of Request and Response are usually (but not always!) generic parameters to a statement, meaning that their available sub-properties will vary based on the generic type. Also, some statement types don’t have Request, and others don’t have either Request or Response. It would sure be nice if I don’t have to consult MSDN documentation or Reflector each time I write a binding statement. Bindings are strongly-typed at runtime, so why not at design-time too?
Then this post popped up in Google Reader and reminded me that I can generate those icky dirty strings from nice shiny expression trees, just like LINQ to SQL generates SQL from strongly-typed C# statements. So my Bindings can go from this:
Statement.CreateResource("feedStatement", null, dataFeed,
Statement.Bind("CollectionUrl",
"folderStatement", "Response.DataFeedsLink"));
to this:
S.CreateResource(dataFeed)
.Bind(df => df.CollectionUrl,
folderStatement, fs => fs.Response.DataFeedsLink);
Due to the way I’ve defined the generic parameters on the lambda expressions, the types of the source property and the target property have to match. If they don’t, you get immediate red squiggly feedback in Visual Studio. I’m not sure if that’s a Visual Studio thing or a Resharper thing, but that’s how it works on my box. At the very least you will find out at compile-time instead of at runtime.
Besides the lambda expressions and the ability to call Bind() on the statement after it has been created, it’s worth noting that the Statement.Name “folderStatement” string has been replaced with a reference to the source Statement itself. No more remembering statement names (until we get to ConditionalStatements that is…).
Simpler statement construction
So what was that “S” thing in the previous example? That’s my static utility class that offers methods equivalent to most of the static factory methods on the Statement class. Methods in “S” have the same names, but they typically have fewer parameters, resulting in a more concise syntax when you’re data binding. They also give you the option of using strings instead of Uri objects.
Yes I know, it’s not fair that my utility class gets the short, easy to type name while “Statement” makes you type twice as much before Intellisense kicks in and wastes a bunch of horizontal space. So put “using S = Microsoft.LiveFX.ResourceModel.Scripting.Statement;” at the top of your code if that makes you feel better. :-)
You may also have noticed that I didn’t supply a name for the statement. All of the factory methods in “S” automatically generate a random statement name so that every statement is inherently bindable. If you need a well-known name for inspecting script results or for use in a ConditionalStatement, you can use the NameStatement() extension method:
S.CreateResource(dataFeed).NameStatement("myName")
NameStatement() also checks for valid statement names so you find out at design-time rather than at runtime that statements can’t start with a number and can’t contain spaces.
Functional statement construction
Once you start writing utility methods to generate groups of statements that you string together into a script, you quickly run into the issue that you’re always having to write little bits of shim code to repackage your statements into a single Statement[] before feeding them into your CompoundStatement of choice (Sequence, Interleave, or Conditional). Wouldn’t it be nice if you could throw anything you wanted into a CompoundStatement and it would all be taken care of, similar to the XElement constructor in LINQ to XML?
If you’re not familiar with the XElement constructor, it looks like this:
public XElement(XName name, params object[] content)
It’s a bit loosey-goosey with the object[] parameter, but according to the documentation it allows you to pass in objects that are (or can be converted to) XML nodes, as well as IEnumerable<T> of such objects. Null content is silently ignored. Anything else results in an exception at runtime.
When this style is applied to CompoundStatement construction, it enables the following code:
S.Sequence(
readMeshObjects,
conditionallyCreateFolders,
createAnotherFolder,
createFiles)
That doesn’t look very interesting without the type declarations, but imagine the first two parameters are different types of Statements, the third parameter is a custom object that implements IEnumerable<Statement>, and the last parameter is a Statement array. You can see this code in action in the IfElseSample in the download.
Binding to URLs and Requests
One of the most common uses of bindings is to perform CRUD operations on a URL that comes from the result of a previous statement. Specifying the target URL in the binding can become quite repetitive. The property name for the target URL also varies by statement type. Sometimes it’s EntryUrl, sometimes it’s CollectionUrl, and sometimes it’s MediaResourceUrl.
I address this with the AtUrl() extension method which eliminates the need to specify the target property and lets you write:
S.CreateResource(dataFeed)
.AtUrl(folderStatement, fs => fs.Response.DataFeedsLink);
A similar WithRequest() extension method exists for binding to the Request property on CreateResource, UpdateResource, and SynchronizeResourceCollection.
Conditional statements
The ConditionalStatement is worth an entire blog post. Until then, here’s an example of the syntax I’ve enabled:
S.If(statement =>
(((ReadResourceCollectionStatement<MeshObjectResource>)
statement.FindStatement("ReadObjects")).Response.Entries
.Where(mo => mo.Title == "My Folder").Count() == 0))
.Then(ScriptHelper.CreateFolder("Folder didn't exist"))
.Else(ScriptHelper.CreateFolder("Folder DID exist")
I should note that ConditionalStatement already exposed the ability to use lambda expressions. All I did was enable the If().Then().Else() syntax. The Else() is optional.
Strongly typed statement groups
Notice the CreateFolder() helper method in the previous example? Originally this method returned a Statement[] containing two statements. The first statement created the MeshObject that represents the folder and the second statement created a DataFeed at the DataFeedsLink of the folder. This Statement[] was sufficient for creating a folder with a given name, but if I wanted to do something interesting with it such as put files in the folder or use a binding to change its title, it quickly became a pain to grab the appropriate entry from the array and cast it to the correct type.
So I created a helper class named CreateFolderStatementPair that exposes strongly typed properties named FolderStatement and FilesFeedStatement. This lets you write:
S.CreateResource<DataEntryResource>()
.AtUrl(folder.FilesFeedStatement, f => f.Response.DataEntriesLink)
and
folder.FolderStatement.Bind(mo => mo.Request.Title, "new title");
CreateFolderStatementPair inherits from an abstract class named StatementGroup which implements IEnumerable<Statement> and also has an implicit operator conversion to Statement[]. Implementing IEnumerable<Statement> means you can pass a StatementGroup into S.Sequence(), S.Interleave(), and the Then()/Else() methods. The implicit conversion to Statement[] means you can pass a StatementGroup into methods that expect a Statement[] such as the original Statement.Sequence() method. You can use StatementGroup to create your own strongly typed group of statements that play well with bindings and with the S.*/Statement.* factory methods.
Miscellaneous helpers
Besides CreateFolder(), The ScriptHelper static class has a few other useful methods. CreateMedia() takes a CreateResourceStatement<DataFeedResource> and an external media URL and does a CreateMedia at the MediaResourcesLink of the CreateResourceStatement. There is also a CreateMedias() method that takes multiple media URLs. ScriptHelper has a few other convenience properties and methods, but nothing noteworthy.
There are a few other extension methods I haven’t mentioned yet.
ToSequence() and ToInterleave() turn a Statement array into a SequenceStatement or an InterleaveStatement respectively.
AddBindings() and AddParameters() let you add bindings and parameters to statements after they have been created.
FindStatement<TStatement>() recursively finds the first statement of the specified type in an IEnumerable<Statement>.
A Compile() method has been added to all statement types, not just CompoundStatements. Run() and RunAtServer() with an implicit Compile() have also been added to any Statement type.
Run() and RunAtServer() no longer require any parameters if you first call ScriptHelper.SetCredential(username, password).
Download
You can download the code here. The solution contains a console app that demonstrates some of the features with a few sample scripts.
To run the sample you will of course need to change the username and password. If the project references to Microsoft.LiveFX.Client.dll, Microsoft.LiveFX.ResourceModel.dll, and Microsoft.Web.dll are broken, you will need to remove and recreate them in both projects.
Conclusion
If you’re wondering why Resource Scripts didn’t have these features already, you need to remember that Resource Scripts were designed to be written using a visual designer tool similar to the Windows Workflow designer. The team was also under intense pressure to make the CTP available in time for PDC.
Think of this library as an experiment to see what a more code-centric API might look like and whether it could co-exist with a visual designer tool. Who knows, maybe some of the concepts such as strongly-typed StatementGroups might find their way into such a visual designer.
Hopefully this library enables and encourages more people to play with Resource Scripts. If you have any feedback, I’d love to hear it. Have fun scripting your Mesh!
No comments:
Post a Comment