(20070917 - updated to Silverlight 1.0)
Sorry for the extended absence. I've been heads down planning for Office "14" Groove, and quite removed from all the Groove 2007 activity. But here's something new and relevant, that I'm quite excited about.
Silverlight - if you didn't already know - is a new plugin, with quite amazing capabilities. Vector graphics, animation, storyboards, a very rich and clean object model... and it's all .NET and XAML, and all running inside a browser. As a platform for lightweight applications, I think it's really powerful.
Groove Forms has a relatively restricted set of user interface capabilities. It's designed for building reasonably straightforward UI for data capture in small teams. Forms with fields, views with columns, and a Groove workspace for distributed data storage. But many applications would like to go beyond a simple forms-driven UI, and Groove Forms doesn't make that very easy.
Silverlight in Groove Forms, though, would be quite a nice combination. So here it is.
This example is very simple: load the Silverlight control, interact with it in the Groove form, save some state into the Groove record, and load the state back into the control when reading or opening a record. In this case the model itself is also very simple: a set of Path elements, drawn by the user. It's a sketchpad. But once you have these simple pieces, a whole set of other exciting things become possible. Charts of data. Interactive collaboration around a shared model...
First let's create a very simple new form, and add four fields:
Here's my version of this form. The "staticControl" field has some placeholder text, just so I can find it easily.
We'll use the JavaScript programming capabilities of Silverlight 1.0. The SDK contains a standard script file "Silverlight.js" to wrap all the important things about initializing the Silverlight control, and we'll use this simply by importing it into the Groove Form. Then we'll write another script of our own.
So, in the "Form Scripts" tab of our new form, let's add two scripts:
In the "System callouts script" for this form, I have some one-line functions that call into SilverlightLoaders.js to do their work:
A Silverlight control can be embedded directly into an HTML page, or can be created at runtime using script. We'll use the latter, since Groove Forms doesn't give you a way to define arbitrary HTML directly in the form page. The Silverlight.js script has some functions to create the control, and embed it within the current page.
The control needs a XAML canvas definition, that will be loaded when it first starts. (Without an initial canvas, the control really doesn't do much of anything at all). There are two ways to supply the XAML: from a URL, or from an element in the page. Since Groove doesn't always need to be connected to a network, we would prefer not to rely on a web server to supply this initial content, but rather distribute it within the Forms tool istelf.
To do this, we write a XAML fragment into the "staticXaml" field declared earlier. In the case I'm showing here, that fragment contains pretty much the whole application we're building. Alternatively it could be just a skeleton -- the canvas -- and the rest of the model created later on the fly.
So here's our initialization script, complete with all the extra gubbins we'll see later.
Here I'll let you read the code first. This is pretty lifted much verbatim from Laurence Moroney's blog; with the additional step of keeping a string array of all the ink paths in memory. We'll use that as the way to load and save ink to Groove, later.
// ===== Ink data and event handling ===== // We keep a (script) array: each element of the array is the xaml text of one drawn line.// This array is stored in the Groove record, as well-formed XML: all the lines, then wrapped in a "Canvas" element.// To load this back in to Groove we parse the data with XMLDOM and walk its child elements,// passing each one to Silverlight in turn. var g_AllPaths = new Array(); var g_CurrentStroke = null; function inkInitialize(){ g_CurrentStroke = null; g_AllPaths = new Array();} // Mouse down event handler from the InkPresenterfunction inkMouseDown(sender,args){ if(!IsEditMode()) { // Do nothing return; } // Capture the Mouse g_AgInk.CaptureMouse(); // Create a new stroke g_CurrentStroke = g_AgControl.content.createFromXaml('<Stroke/>'); // Assign a new drawing attributes element to the stroke // This, as its name suggests, defines how the stroke will appear var da = g_AgControl.content.CreateFromXaml('<DrawingAttributes/>'); g_CurrentStroke.DrawingAttributes = da; // Now that the stroke has drawing attributes // Let's define them... g_CurrentStroke.DrawingAttributes.Width = 1; g_CurrentStroke.DrawingAttributes.Height = 1; g_CurrentStroke.DrawingAttributes.Color = "Black" g_CurrentStroke.DrawingAttributes.OutlineColor = "Black" g_CurrentStroke.StylusPoints.AddStylusPoints(args.GetStylusPoints(g_AgInk)); g_AgInk.Strokes.Add(g_CurrentStroke);} // Add the new points to the Stroke we're working withfunction inkMouseMove(sender,args){ if(!IsEditMode()) { // Do nothing return; } if (g_CurrentStroke != null) { g_CurrentStroke.StylusPoints.AddStylusPoints(args.GetStylusPoints(g_AgInk)); }} // Release the mousefunction inkMouseUp(sender,args){ if(!IsEditMode()) { // Do nothing return; } var pathString = "" var pathElement = "<Path Stroke='Black' Data='" var n = g_CurrentStroke.StylusPoints.Count; for(i=0;i<n;i++) { if(i==0) { pathString +="M " } else { pathString +="L " } pathString+= + g_CurrentStroke.StylusPoints.GetItem(i).X + "," + g_CurrentStroke.StylusPoints.GetItem(i).Y + " " } pathElement+=pathString+"' />" addPath(pathElement); g_AgInk.Strokes.Remove(g_CurrentStroke); g_CurrentStroke = null; g_AgInk.ReleaseMouseCapture();} // ===== // Add a path (xaml string) to the current drawing// and to the in-memory array of path stringsfunction addPath(path){ var elPath = g_AgControl.content.createFromXaml(path); g_AgCanvas.children.add(elPath); g_AllPaths[g_AllPaths.length] = path;}
To save the user's drawing into Groove, we use the form's OnBeforeSubmitData callback:
So when you hit Save, the current version of the model is stored away in the Groove record, and diseminated to any other members in the workspace.
// Save current array of paths into the Groove record// (writing into the UI document, not the underlying record directly)function savePaths(){ var xaml = "<Canvas>" + g_AllPaths.join("") + "</Canvas>"; SetHTMLFieldValue("Data", xaml);}
To load a drawing from the current record -- when previewing a record, or when opening an existing record for editing -- I took a slightly more convoluted route. I read the value of the Data field from the current record, and parse it with a regular XMLDOM object. This gives us some advance validation that the data is well-formed XML, and it also makes it really easy to separate out each of the Path elements, then load them up into the Silverlight control.
// Load paths from an existing Groove recordfunction tryLoadPaths(){ // The Silverlight control loads asynchronously, so wait until it's called back to the page. if(g_AgLoaded && g_AgControl && g_AgRoot && g_AgCanvas && g_AgInk) { GetApp().ClearStatusBarMessage(); loadPaths(); return; } GetApp().DisplayStatusBarMessage("Waiting for Silverlight to initialize...", GrooveMessageBoxIcon_None ); window.setTimeout( tryLoadPaths, 100 );} function loadPaths(){ try { if(g_AgCanvas.children) { // Clear any existing paths from the canvas g_AgCanvas.children.clear(); } var xaml = GetHTMLFieldValue("Data"); if(xaml) { var dom = new ActiveXObject("Microsoft.XMLDOM"); dom.loadXML(xaml); var p = dom.documentElement; for( var j=0; j<p.childNodes.length; j++ ) { var path = p.childNodes(j).xml; addPath(path); } } } catch(e) { alert( "LoadPaths: " + e.description); }}
There's one little wrinkle with the loader function: Silverlight might not be ready yet. This happens, for example, when editing an existing Groove record: Groove calls OnBeforeInitialize, we spin up the Silverlight control and have it load the initial canvas; Groove loads the data record, and calls the OnAfterInitialize form script, but Silverlight hasn't yet called back to say it's ready. To avoid this case, I just set a little timer (window.setTimeout) to try again a few milliseconds later.
All done!
Here's the tool template. Use at your own risk, not production quality, no warranties and so on.