Welcome to MSDN Blogs Sign in | Join | Help

Usability Studies

Do you use SharePoint and work with Groove? Microsoft's User Research Group is conducting series of studies for SharePoint products and technologies at the Microsoft campus in Redmond, WA, and is looking for participants in the Puget Sound area

The research team is looking for individuals who use SharePoint at least twice a week and have experience working with Groove. Each participant will receive a gift item they select from a list of some of Microsoft's most popular hardware and software titles.

If you are interested please email itusable@microsoft.com with your name and phone number, with "Groove" in the subject line.

Posted by hpyle | 1 Comments

Groove, Step By Step

New from Microsoft Press: Microsoft® Office Groove® 2007 Step by Step, by Rick Jewell and John Pierce.

Sounds like a good one.  Rick has been doing this Groove stuff for a while, and knows his subject well.

 

Posted by hpyle | 0 Comments

Silverlight in Groove

(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...

A simple form

First let's create a very simple new form, and add four fields:

  • A text field named "Title".  This is just so we can display something in the view.
  • A static text field named "staticXaml": hidden.  We'll use this to bootstrap the Silverlight control.
  • A static text field named "staticControl".  This will contain the actual Silverlight control in the form.
  • A text field named "Data": hidden.  We'll use this to store some data that the user created.

Here's my version of this form.  The "staticControl" field has some placeholder text, just so I can find it easily.

Some scripts

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:

  • Silverlight.js, imported from the SDK, and
  • SilverlightLoaders.js, a new script we'll write here.

In the "System callouts script" for this form, I have some one-line functions that call into SilverlightLoaders.js to do their work:

  • in the OnBeforeInitialize() function:  a call to form_OnBeforeInitialize();
  • in the OnAfterInitialize() function:  a call to form_OnAfterInitialize();
  • in the OnBeforeSubmitData() function:  a call to form_OnBeforeSubmitData();

Loading the Silverlight control

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.

// REQUIRES:
// (hidden) static text field, named "staticXaml", to hold the bootstrap xaml
// (visible) static text field, named "staticControl", where the control will be sited
// (hidden) text field, named "Data", to store the data in
 
// Globals:
var g_AgLoaded = false;           // The silverlight control has loaded
var g_AgControl = null;
var g_AgRoot = null;
var g_AgCanvas = null;
var g_AgInk = null;
 
 
// ===== Groove callbacks (form events) =====
 
// Form is being initialized for the first time
function form_OnBeforeInitialize()
{
       inkInitialize();
       createSilverlightControl();
}
 
// Form is being initialized, and Groove data is ready
function form_OnAfterInitialize()
{
       inkInitialize();
       tryLoadPaths();
}
 
function form_OnBeforeSubmitData()
{
       savePaths();
}
 
 
 
// ===== Silverlight control initialization =====
 
function createSilverlightControl()
{
       if(g_AgLoaded && g_AgControl)
       {
              // The silverlight control is already loaded and ready.
              return;
       }
 
       // Create a "script" tag containing an empty canvas.
       // This canvas will be the first item loaded into the Silverlight control.
 
       var c = '<Canvas x:Name="staticCanvas" xmlns="http://schemas.microsoft.com/client/2007" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Width="640" Height="480">' +
              '<InkPresenter x:Name="inkEl" Background="transparent" Width="500" Height="500" MouseLeftButtonDown="inkMouseDown" MouseMove="inkMouseMove" MouseLeftButtonUp="inkMouseUp" />' +
              '<Canvas x:Name="pathsCanvas" />' +
              '</Canvas>';
 
       var s = document.createElement("script");
       s.setAttribute("type", "text/xaml");
       s.setAttribute("id", "initialXaml");
       s.text = c;
       document.getElementById("staticXaml").appendChild(s);
 
       // Create the Silverlight control,
       // which will then call the "canvas_loaded" function when ready.
 
       Silverlight.createObjectEx({
              source: '#initialXaml',
              parentElement:document.getElementById("staticControl"),
              id:'AgControl1',
              properties:{
                     width:'1024',
                     height:'1024',
                     background:'transparent',
                     inplaceInstallPrompt:true,
                     isWindowless:'true',
                     framerate:'24',
                     version:'0.90.0'},
              events:{
                     onError:null,
                     onLoad:canvas_Loaded},
              context:null
       });
 
}
 
// Callback from Silverlight when the initial canvas has been loaded
function canvas_Loaded(control, userContext, rootElement)
{
        g_AgControl = control;
        g_AgRoot = rootElement;
       g_AgCanvas = control.content.findName("pathsCanvas");
       g_AgInk = control.content.findName("inkEl");
       g_AgLoaded = true;
}
 
 
// ===== Utils, misc =====
 
function IsEditMode()
{
       return( !GetIsSearch() && !GetIsPreviewPane() && !GetIsReadOnly() ) ;
}

Ink and Paths

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 InkPresenter
function 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 with
function inkMouseMove(sender,args)
{
       if(!IsEditMode())
       {
              // Do nothing
              return;
       }
       if (g_CurrentStroke != null)
       {
              g_CurrentStroke.StylusPoints.AddStylusPoints(args.GetStylusPoints(g_AgInk));
       }
}
 
// Release the mouse
function 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 strings
function addPath(path)
{
       var elPath = g_AgControl.content.createFromXaml(path);
       g_AgCanvas.children.add(elPath);
       g_AllPaths[g_AllPaths.length] = path;
}

Load and Save

To save the user's drawing into Groove, we use the form's OnBeforeSubmitData callback:

  • Join the array of paths we know about
  • Wrap them all inside a <Canvas> tag
  • Write this to the "Data" field on the form.

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 record
function 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.

Posted by hpyle | 6 Comments

The Light of Day

Microsoft Office Groove 2007, Trial Product, in English and Spanish, for download.

http://us1.trymicrosoftoffice.com/product.aspx?family=officelivegroove&culture=en-US

Posted by hpyle | 1 Comments

Forms Script: Send Instant Message

I want to take you line-by-line through this code, with maybe a few words of explanation for each piece.

 // Find the contact
 var pContact = GetContactFieldValue("Contact");

Groove contacts are a datatype unto themselves; the contact object contains a ContactURL (the main unique identifier for this person's Groove identity; this is an opaque string), their VCard (with name, address, telephone numbers, email, and so on), and some internal stuff which provides enough information to allow Groove communicate with this user:  the URL of their relay server(s) and their public key.

To send an instant message to a contact, you actually just need their contact URL, but the contact must be "known" as well:  in other words, although the API to send a message just wants the contact URL, it needs to look up that contact by URL in your account's database of known contacts, in order to send an encrypted message across the network to that user.  This "contact store" is a whole other subject, which I'm happy to delve into sometime.  But for now, the Contact field type gives us easy access to a known contact, and the contact's URL.

GetContactFieldValue() is one of the built-in script functions.  In the Forms developer documentation (which is part of the SDK), you can find the full list of public script functions; alternatively you can dive right in to the PublicFunctions.js script file which is located in Program Files\Microsoft Office\OFFICE12\Groove\ToolData\groove.net\GrooveForms5.

 if(!pContact)
 {
  GetApp().DisplayError("Please specify a contact.");
  return;
 }

GetContactFieldValue() returns the contact object (a IGrooveFormsToolContact) which is contained in the specified field.  If no contact has been selected, the return value is null, so we display an error message and return.

To display the error message, I could have used "alert()", since the form page is just an Internet Explorer document.  But you get a nicer dialog box by using the DisplayError function.

GetApp() returns the Forms tool's "UI delegate": the entrypoint into a collection of Groove-related functions and capabilities.  In this case, DisplayError is just a method on the UI delegate itself.  You can also access the other script interfaces and their functions by a syntax like, for example

GetApp().IGrooveFormsToolApplicationPreferences.DoesPreferenceExist("something")

For a full list of these other interfaces and functions, again, refer to the forms developer documentation in the SDK.

 if(!pContact.IsGroovey)
 {
  GetApp().DisplayError("Please specify a Groove contact.");
  return;
 }

We know we have a IGrooveFormsToolContact object, and this is an extra safety check to ensure that the contact is "groovey": that the contact represents a Groove user, rather than just an email address or other contact who can't receive Groove instant messages.  It's actually quite hard to create non-groovey contacts in Groove 2007, but you might come across them occasionally.

 var enumContacts = CreateBSTREnumFromArray([pContact.ContactURL]);

The pContact.ContactURL gives us the URL of the selected contact (which is a string).  Wrapping this in square brackets is JavaScript's syntax to create a single-element array.  Then we convert the script array into a BSTREnum, because the SendInstantMessage method we want to call later takes a IGrooveBSTREnum as the list of recipients.  A BSTREnum is an (unordered, strictly) enumeration of strings.  CreateBSTREnumFromArray() is one of the public script functions.

 // Get the message text
 var sMessage = GetHTMLFieldValue("Message");

Reads the text contents of the Message field into a string.  GetHTMLFieldValue() is another of the public script functions.

 // Send message
 GetApp().SendInstantMessage(enumContacts,sMessage);

Sends an instant message, with the message body specified in sMessage, to the list of contacts in enumContacts.  The body is just plain text.  There are some optional parameters to this method, which we're just ignoring here: whether to track the message delivery, and whether to save the sent message into your message history.  There's also an interesting alternate method, SendInstantMessageWithLinks, which can embed Groove hyperlinks into the message.

And that's it.  A simple form which sends instant messages when you press a button.  Extensions of this technique can be used to send messages automatically when a form is saved or particular conditions are met.  I hope this little walkthrough has covered a few of the not-completely-obvious ways in which Groove Forms script hangs together.  Let me know other subjects you'd like to see given the same treatment!

Posted by hpyle | 0 Comments
Filed under:

Forms Script: Send Instant Message

Sorry about the hiatus here.  It's been a while.

Here's a little piece of script code for Groove Forms, which might be useful to a few people.  It's also small enough to be a nice illustration of some of how script and forms work together.

The application itself is small enough to be nearly trivial: make a form with a button on it which sends an instant message to someone.  So, let's start with a new forms tool; create a contact field called "Contact", and a multi-line text field called "Message", and a script button called "Send".  In the designer, it looks like this:

In the fields list, double-click the "Send" button-field to modify its properties; under "OnClick", enter a line of script code which calls a function we'll write in a second:

SendIM()

And save.  Then, "Create new script", and paste in this script code.

function SendIM()

{

// Find the contact

var pContact = GetContactFieldValue("Contact");

if(!pContact)

{

GetApp().DisplayError("Please specify a contact.");

return;

}

if(!pContact.IsGroovey)

{

GetApp().DisplayError("Please specify a Groove contact.");

return;

}

var enumContacts = CreateBSTREnumFromArray([pContact.ContactURL]);

// Get the message text

var sMessage = GetHTMLFieldValue("Message");

// Send message

GetApp().SendInstantMessage(enumContacts,sMessage);

}

Save.  Create a view.  Publish sandbox.  New -> Form, and test it out.  Everything should work: you can pick a contact in the contact field, write a message, and send the message to that contact.

Now let's look at how this all fits together.

Posted by hpyle | 0 Comments
Filed under:

Script Performance

If you're building anything complex with the Groove Forms tool, you probably use plenty of script code.  Peter Gurevich on the IE team has a great article about how to get the best performance.  I'm looking forward to parts two and three, too.
Posted by hpyle | 0 Comments

Office Developer Conference

MSDN have published all the session videos from the Office Developer Conference in March. This is the deepest presentation-type dive we've taken into Groove 2007 application development – and for three hours' attention you'll probably not find a better intro anywhere. In my humble opinion (-_-).

 

(The MSDN site's descriptions of CB302 and CB304 are switched around – they're correct below):

 

# CB302—Groove 2007: Developing Applications

 

Presenter: Hugh Pyle; 77 minutes (280 MB)

Get an overview of Groove and its application development environments: Groove Forms and InfoPath Forms for workgroup applications; Groove Web Services for programmatic access to Groove services and data; and the Groove Data Bridge systems integration platform.

 

# CB304—Groove 2007: Building Forms-Based Workgroup Applications

 

Presenter: Joshua Mahoney; 64 minutes (148 MB)

Learn how to use the Groove Forms and InfoPath Forms tools to build applications for team collaboration and structured data within Groove workspaces.

 

# CB303—Groove 2007: Web Services, Data Bridge and Systems Integration

 

Presenter: Hugh Pyle; 72 minutes (275 MB)

Learn how the Groove Web Services API provides programmers and professional developers access to the collaborative services of Groove 2007, including Groove Forms, InfoPath Forms, and files within Groove workspaces.

Posted by hpyle | 3 Comments

GWS and SharePoint

John Milan, from Bellevue-based TeamDirection, is blogging about Groove and SharePoint integration using each product's web services APIs.  Worth keeping an eye on.
Posted by hpyle | 0 Comments

Files web service

Let's move along quickly to the Files web service.  On the newsgroup, someone's been having problems creating files within folders in a Files tool, so this seems like good timing.

A bit of background, first, to set the stage for some of the little complexities we'll probably come across later.

The Groove Files tool is a container for binary objects (files) in a folder tree.  There's a single root folder at the top of the tree, which you can't delete or rename. You can use web services methods to read the list of contents of a folder (recursively, or one level at a time), and to create, read, update and delete files and folders.  Events are provided so you can subscribe for notification when files/folders are modified in the tool.

Unlike the Forms tool, which is all about structured data, the Files tool doesn't have an extensible way for you to add custom metadata to a file or folder within the tool.  A file consists of a filename, some information about the creator/modifier, an ID, and maybe some file contents.  So, where you need extended metadata attached to binary objects, the Files tool might not be the ideal place to store that information within a Groove workspace; you might find it more convenient to use a Forms tool with an Attachments field for the binary contents.

On the other hand, Files tools have a couple of built-in features not available with Forms attachments:  binary-difference optimised change handling, and download-on-demand content.

Binary diffs are transparent to the user (and the application developer) but can make a substantial performance difference in bandwidth-constrained environments.  The diff process takes each update to a file and compares it to the previous version of the file's contents; if only a small region of the file has changed, then only those bits are sent across the network to other users.  (The tradeoff here is bandwidth against CPU.  If the diff is not much smaller than the original file, or if calculating the difference regions takes a while, then the whole file contents are sent instead).

Download-on-demand is controllable by users, and affects web services application developers too.  This feature (sometimes called "asymmetric files") separates the file stub (name, author, etc) from its contents; each member of the workspace can choose whether the actual file contents are downloaded to their machine.  This is set per folder, and the user interface has a few options:

As a developer, you'll see that asymmetric files are basically enabled all the time.  Every time you want to read a file's contents, you should check its download status first, otherwise you'll hit an exception.

 

 

Posted by hpyle | 0 Comments

Newsreaders and code samples

Sorry to anyone trying to make sense of these code samples with a newsreader; you'll miss all the formatting.  I should take a look at how these folks do it -- seems to work well for them.  Meanwhile, to see slightly-better-formatted code, you'll need to use a browser.

Posted by hpyle | 0 Comments

Forms Attachments

In Groove Forms, you can add an Attachments field to your forms, and store any number of attachments in the field.  (You can define more than one attachments field in the tool, but each form can only include one attachments field).

Let's look at accessing these attachments with web services.

Here's the schema of a form with one attachments field:

<xs:complexType>
<xs:sequence>
<xs:element name=\"Attachments\" type=\"FileAttachments\" minOccurs=\"0\" />
<!-- ... the other form fields ... --!>
</xs:sequence>
</xs:complexType>

The FileAttachments type is also defined in the schema; you can see the specification in Forms2.xsd in the development kit.  It consists of three linked types:

  • An unbounded sequence of FileAttachment elements; each of which is...
  • A sequence of {ID, Created, CreatedBy, Modified, ModifiedBy, FullName, Type, Size, and Contents}, where Contents is...
  • A Base64 binary value.

When you read this structure into a DataSet, each of these types is represented as a separate table, with child relationships between the form, the FileAttachments, each FileAttachment, and its FileContents.

Convenient? Not without a small amount of code to wrap access to the attachments.  So let's write the code.

My model here is to build a helper class which you can construct from a DataRow of forms data.  This helper should provide nice easy methods to enumerate existing file attachments, and to create and delete attachments too.  So here we go:

public class GrvFormsRecord

{

private DataRow _row;

/// Construct a GrvFormsRecord from the DataRow representing the form data.

public GrvFormsRecord(DataRow dataRow)

{

_row = dataRow;

}

public bool SupportsAttachments

{

get

{

return (_row != null && _row.Table.ChildRelations.Count > 0);

}

}

private DataRow[] AttachmentRows

{

get

{

DataRow[] attachmentRows = new DataRow[0];

if (SupportsAttachments)

{

DataRelation relationForAttachProp = _row.Table.ChildRelations[0];

DataRow[] attachPropRows = _row.GetChildRows(relationForAttachProp);

if (attachPropRows.Length > 0)

{

DataRow attachPropRow = attachPropRows[0];

if (attachPropRow.Table.ChildRelations.Count > 0)

{

DataRelation relationForFileAttach = attachPropRow.Table.ChildRelations[0];

attachmentRows = attachPropRow.GetChildRows(relationForFileAttach);

}

}

}

return attachmentRows;

}

}

}

The SupportsAttachments method is simple.  If the row doesn't define any child relationships, then your form doesn't include any FileAttachment-type fields, so you can't attach files to it.

Then there's a (private) getter to find the actual rows representing each file attachment.  This finds the single child row in the FileAttachments table, which represents the FileAttachments data type; then returns an array of all the FileAttachment child rows.  Each of these rows has columns for the attachment's metadata: FullName, Size, and so on.

So far, so good.  Now it's easy to write this:

public int AttachmentCount

{

get

{

return AttachmentRows.Length;

}

}

Next, I'll create a separate class to represent the attachment itself.  Once we have such an object, the rest of my GrvFormsRecord class is easy too: provide an enumerator for attachments, and methods to create and delete attachments from the record.

public IEnumerable<GrvFormsRecordAttachment> Attachments

{

get

{

foreach (DataRow attachmentRow in AttachmentRows)

{

GrvFormsRecordAttachment att = new GrvFormsRecordAttachment(attachmentRow);

yield return att;

}

}

}

public GrvFormsRecordAttachment CreateAttachment(string name, byte[] content)

{

if (!SupportsAttachments)

{

throw new Exception("Record does not support attachments.");

}

return new GrvFormsRecordAttachment(_row, name, content);

}

public void DeleteAttachment(GrvFormsRecordAttachment att)

{

att.Delete();

}

And this makes the application-level code look really simple.  Something like this,

// Application code using GrvFormsRecord...

DataRow dr = formsDataTable.NewRow();

GrvFormsRecord rec = new GrvFormsRecord(dr);

if (rec.SupportsAttachments)

{

string fileName = // something.txt

byte[] fileContents = // the attachment contents

rec.CreateAttachment(fileName, fileContents);

}

formsDataTable.Rows.Add(dr);

My GrvFormsRecordAttachment class wraps the attachment-metadata row and the related FileContents row into a single object.  Here I've made the constructors "internal", since the only way I want to create one of these objects is via the GrvFormsRecord class above.  Without further explanation, then, here's the rest of the code.

/// <summary>

/// Helper class representing an attachment to a Forms record.

/// Don't construct this directly; use the GrvFormsRecord class methods.

/// </summary>

public class GrvFormsRecordAttachment

{

DataRow _attachmentRow;

DataRow _contentsRow;

// Construct from an existing row

internal GrvFormsRecordAttachment(DataRow attachmentRow)

{

_attachmentRow = attachmentRow;

DataRelation relationForContent = _attachmentRow.Table.ChildRelations[0];

DataRow[] contentRows = _attachmentRow.GetChildRows(relationForContent);

_contentsRow = contentRows[0];

}

// Construct a new attachment from name and content

internal GrvFormsRecordAttachment(DataRow recordRow, string name, byte[] content)

{

DataRelation relationForAttachProp = recordRow.Table.ChildRelations[0];

DataRow[] attachPropRows = recordRow.GetChildRows(relationForAttachProp);

DataRow attachPropRow;

if (attachPropRows.Length == 0)

{

DataTable propertiesTable = relationForAttachProp.ChildTable;

attachPropRow = propertiesTable.NewRow();

propertiesTable.Rows.Add(attachPropRow);

attachPropRow.SetParentRow(recordRow);

}

else

{

attachPropRow = attachPropRows[0];

}

// Create the row in the FileAttachment table

// Note: the attachment's name is the FullName field.

// in Groove V3.1 this was two properties, Name (filename with extension) and DisplayName (filename without extension)

DataRelation relationForFileAttach = attachPropRow.Table.ChildRelations[0];

DataTable fileAttachmentTable = relationForFileAttach.ChildTable;

_attachmentRow = fileAttachmentTable.NewRow();

_attachmentRow["FullName"] = name;

_attachmentRow["Type"] = "File";

_attachmentRow["Size"] = content.Length;

_attachmentRow.SetParentRow(attachPropRow);

fileAttachmentTable.Rows.Add(_attachmentRow);

// Create the row in the Contents table

// and set the binary contents of the attachment

DataRelation relationForContent = _attachmentRow.Table.ChildRelations[0];

DataTable contentsTable = relationForContent.ChildTable;

_contentsRow = contentsTable.NewRow();

_contentsRow["Base64"] = content;

_contentsRow.SetParentRow(_attachmentRow);

contentsTable.Rows.Add(_contentsRow);

}

// Delete this attachment.

internal void Delete()

{

_contentsRow.Delete();

_contentsRow = null;

_attachmentRow.Delete();

_attachmentRow = null;

}

/// <summary>

/// Filename of the attachment. Read-write.

/// </summary>

public string Name

{

get

{

return (string)_attachmentRow["FullName"];

}

set

{

_attachmentRow.BeginEdit();

_attachmentRow["FullName"] = value;

_attachmentRow.EndEdit();

_attachmentRow.AcceptChanges();

}

}

/// <summary>

/// Size of the attachment, in bytes. Read-only.

/// </summary>

public long Size

{

get

{

return (long)_attachmentRow["Size"];

}

}

/// <summary>

/// The attachment contents. Read-write.

/// </summary>

public byte[] Content

{

get

{

byte[] data = (byte[])_contentsRow["Base64"];

return data;

}

set

{

_attachmentRow.BeginEdit();

_attachmentRow["Size"] = value.Length;

_attachmentRow.EndEdit();

_attachmentRow.AcceptChanges();

_contentsRow.BeginEdit();

_contentsRow["Base64"] = value;

_contentsRow.EndEdit();

_contentsRow.AcceptChanges();

}

}

}

This pair of classes is simple and reusable enough to just drop in to any Forms-tool application projects, making forms attachments access a snap.

Posted by hpyle | 0 Comments

EnforceConstraints

It's really nice to see people try using the techniques I'm posting here.  But, as usual, there are glitches when this stuff comes in contact with the real world.  Here's a really relevant comment from a user:

"When I try to insert a new DataRow to the first table of the Dataset (my Groove form) I got an error since it says that the RecordURI is required. I'm confused since I don't want to insert the RecordURI, the CreatedURL the modifiedURL etc..."

In my code to create a DataSet from the RecordDataSet structure which is used by the GrooveForms2 service, there's a line which this user had missed out.  It says:

recordDataSet.EnforceConstraints = false;

Turns out, if you relax the DataSet's constraints, you can insert records just fine.  To understand why, let's look at the XML version of the schema for a very simple form with only one text field:

<xs:complexType>
<xs:sequence>
<xs:element name=\"MyFormsField\" type=\"xs:string\" minOccurs=\"0\" />
<xs:element name=\"RecordURI\" type=\"xs:string\" />
<xs:element name=\"_ParentID\" type=\"xs:double\" />
<xs:element name=\"_RecordID\" type=\"xs:double\" />
<xs:element name=\"_Created\" type=\"xs:dateTime\" />
<xs:element name=\"_CreatedBy\" type=\"xs:string\" />
<xs:element name=\"_CreatedByURL\" type=\"xs:string\" />
<xs:element name=\"_Modified\" type=\"xs:dateTime\" />
<xs:element name=\"_ModifiedBy\" type=\"xs:string\" />
<xs:element name=\"_ModifiedByURL\" type=\"xs:string\" />
<xs:element name=\"Forms_Tool_grooveFormID\" type=\"xs:double\" />
<xs:element name=\"_Editors\" type=\"xs:string\" />
<xs:element name=\"_Readers\" type=\"xs:string\" />
<xs:element name=\"_UnreadFlag\" type=\"xs:double\" />
</xs:sequence>
</xs:complexType>

The user-defined field says minOccurs=0.  All the system fields, on the other hand, don't have this; they are "required".  So the DataSet constraints prevent you from adding a row which doesn't specify values for the system fields.

To which there are three possible solutions:

  • Relax constraints in the DataSet.  This is the recommended route.
  • Remove all the system columns from the DataSet before you insert rows.  This is a really good choice, since it reduces the amount of data sent in the SOAP packet when you insert or update records in the tool.  But it's slightly more work than just relaxing constraints.
  • Set arbitrary values in these fields.  Don't do this.

The bottom line: use EnforceConstraints=false.  It's safe and easy.

Posted by hpyle | 0 Comments

Update Forms data

Updating Forms data is just about as easy as creating new records:

  • Read the schema and the set of data which you want to update;
  • Update records in your DataSet with appropriate field values;
  • Call the Forms UpdateRecords() method to update the specified records.

There are a few subtleties, though.  First of all: you don't always need to query for the records you'll be updating; if you already know their RecordURIs, that's enough.  So you might use something like this instead:

  • Know a list of RecordURIs which should be updated;
  • Read the schema of the form, constructing a DataSet with this schema but no records;
  • Create new records in your DataSet, each having a RecordURI and the new field values for that record;
  • Call the Forms UpdateRecords() method.

Secondly, if you don't want to update every field in a record, you really don't need to include the unchanged fields in the dataset parameter to UpdateRecords().  The update data set only needs to include, at a minimum, the RecordURI field and the fields which are being changed.  (This can lead to some performance benefits, if you're only updating one or two fields on a wide record).

For example, if you want to set Status field to "Overdue" for all records whose "DueDate" value is in the past, you could implement like this:  removing the unnecessary columns will improve the performance of the web services call.

  • Query (for schema and data) where String.Format("DueDate < #{0}#", nowstring);
  • Update records in the DataSet with new values for the Status field;
  • Remove un-needed columns from the table's schema;
  • Call the Forms UpdateRecords() method.

NOTE: None of these examples are transactionally safe.  You're making two separate web services calls: one to retrieve a set of data (or their RecordURIs), and another to update those records.  In between those two calls, the data might have changed: field values might be different already, or the records might have been deleted, or other records might have been created in the tool which you don't know about yet.  Groove Web Services doesn't include any mechanisms to create a "lock" on the data across multiple calls, so you need to use some appropriate defensive strategy.  I should blog about some approaches for dealing with this type of update "safely", but that will have to wait for another day.

Anyhow; here's some code.  First, my Update method in my helper class:

public void Update(System.Data.DataSet dataSet)

{

if (!IsDesignInitialized)

{

throw new Exception("Forms tool design is not initialized.");

}

GrooveForms2WebService.Forms2RecordDataSet formsDs = new GrooveForms2WebService.Forms2RecordDataSet();

dataSet.AcceptChanges();

string str = dataSet.GetXmlSchema();

XmlDocument xmlDoc = new XmlDocument();

xmlDoc.LoadXml(str);

XmlElement[] schemaArray = new XmlElement[1];

schemaArray[0] = xmlDoc.DocumentElement;

formsDs.Schema = schemaArray;

str = dataSet.GetXml();

xmlDoc.LoadXml(str);

XmlNodeList nodelist = xmlDoc.DocumentElement.ChildNodes;

XmlElement[] dataArray = new XmlElement[nodelist.Count];

int j = 0;

foreach (XmlNode node in nodelist)

{

dataArray[j] = (XmlElement)node;

j++;

}

formsDs.Data = dataArray;

GrooveForms2WebService.GrooveForms2 svc = GetDataService();

svc.UpdateRecords(formsDs, true);

}

Then a short example following the third approach above, to set a Status field.

GrvFormsTool tool = new GrvFormsTool( . . . . );

// Query for overdue records

string nowstring = DateTime.Now.ToUniversalTime().ToString("yyyy'-'MM'-'ddTHH':'mm':'ss'.'fff'Z'");

string query = String.Format("DueDate < #{0}#", nowstring);

DataSet ds = tool.Query(query);

if (ds.Tables.Count > 0)

{

// Assume we already know which form (==table)

DataTable dt = ds.Tables[0];

// Set the status field on each row

foreach (DataRow dr in dt.Rows)

{

dr["Status"] = "Overdue";

}

// Remove columns other than Status and RecordURI

List<string> columnsToDelete = new List<string>();

foreach (DataColumn dc in dt.Columns)

{

string cn = dc.ColumnName;

if (cn != "RecordURI" && cn != "Status")

{

columnsToDelete.Add(cn);

}

}

foreach (string c in columnsToDelete)

{

dt.Columns.Remove(c);

}

dt.AcceptChanges();

tool.Update(ds);

}

(OK, I'm sure there are more concise ways to deal with a DataSet than this.  My ignorance is all over.)

That caveat, again: this is not transactionally safe, so all sorts of things might happen 'twixt query and update.

Posted by hpyle | 0 Comments

Insert Forms data

It's quite straightforward to create new Forms records:

  • Read the Forms tool's schema into a DataSet;
  • Add new records to the DataSet with appropriate field values;
  • Call the Forms CreateRecords() method to insert the new records.

A single call to CreateRecords() will insert as many new records as contained in your dataset; you don't need to make a separate call for each one.  (It's quite a lot more efficient to create multiple records at once).

Here's the code I use to make an Insert() method in my wrapper class.  It takes a System.Data.DataSet, writes its schema and data to the GrooveForms2WebService RecordDataSet, and calls the CreateRecords() method on the web service:

public string[] Insert(System.Data.DataSet dataSet)

{

if (!IsDesignInitialized)

{

throw new Exception("Forms tool design is not initialized.");

}

GrooveForms2WebService.Forms2RecordDataSet formsDs = new GrooveForms2WebService.Forms2RecordDataSet();

dataSet.AcceptChanges();

string str = dataSet.GetXmlSchema();

XmlDocument xmlDoc = new XmlDocument();

xmlDoc.LoadXml(str);

XmlElement[] schemaArray = new XmlElement[1];

schemaArray[0] = xmlDoc.DocumentElement;

formsDs.Schema = schemaArray;

str = dataSet.GetXml();

xmlDoc.LoadXml(str);

XmlNodeList nodelist = xmlDoc.DocumentElement.ChildNodes;

XmlElement[] dataArray = new XmlElement[nodelist.Count];

int j = 0;

foreach (XmlNode node in nodelist)

{

dataArray[j] = (XmlElement)node;

j++;

}

formsDs.Data = dataArray;

GrooveForms2WebService.GrooveForms2 svc = GetDataService();

return svc.CreateRecords(formsDs);

}

The return value is an array of RecordURIs of the newly-created records.

To call this, here's an example setting text fields to random values, in 100 new records:

GrvFormsTool tool = new GrvFormsTool( . . . .);

// Construct a DataSet matching the tool's schema

DataSet ds = tool.GetSchema();

if (ds.Tables.Count > 0)

{

// First table in the DataSet corresponds to the first form

// (you should usually open the table by name...!)

DataTable dt = ds.Tables[0];

// Insert 100 records with text fields set to random values

for (int j = 0; j < 100; j++)

{

DataRow dr = dt.NewRow();

foreach (DataColumn c in dt.Columns)

{

if (!ReservedFormsFields.Contains(c.ColumnName))

{

if (c.DataType is String)

{

dr[c] = Guid.NewGuid().ToString();

}

}

}

dt.Rows.Add(dr);

}

tool.Insert(ds);

}

Of course I'm expecting you already know which fields you want to set.

Note that the record's schema includes several "internal" fields, which you generally don't want to write values for (they'll be assigned automatically by Groove as appropriate).  Some of these fields are writable in some circumstances: _CreatedByURL can be set when a new record is created, for example, but is read-only thereafter.  There's more documentation in the SDK.  In this example I've wrapped this list of reserved fields in a small static helper property:

private static string[] _excl = {

"RecordURI",

"_RecordID",

"_ParentID",

"_Modified",

"_ModifiedBy",

"_ModifiedByURL",

"_Created",

"_CreatedBy",

"_CreatedByURL",

"_Editors",

"_Readers",

"_IgnoreUnread",

"_UnreadFlag",

"Forms_Tool_grooveFormID",

"Forms_Tool_IPContents" };

private static List<string> ReservedFormsFields

{

get

{

return new List<string>(_excl);

}

}

 

Posted by hpyle | 3 Comments
More Posts Next page »
 
Page view tracker