Objects in perma-URLs

re-motion supports passing the IDs of domain objects in URLs, so that you can send around pages like these as links and have the recipient see exactly what you see:

Sending around pages associated with a particular object can be very practical, and many content management systems support this. For example, if I share my PhoneBook application with other people, I can send my fried Josh Szygyäivsük (Polish mother, Finnish father) a link to his EditPersonForm and ask him to correct the spelling of his last-name if I got it wrong and to supplement his address, something like:

Hi Josh!
I've entered your name and phone-number into my corporate
database. Did I remember your name right? Please check
it here:
http://gantarsphonebook.net/EditPerson.wxe?objid=Person|5ffcb701-ff01-446a-b0a4-288bafe9d947|System.Guid
And while you're at it: Please supplement your address, so that
I can send you a christmas card!
Thanks bro
G*

Note that in this e-mail, the URL for the EditPersonForm sports an URL-parameter with some object ID, presumably the one for Josh's Person domain object instance. If Josh clicks on the URL, the server will send a form with his Person object (i.e. with the ID/GUID c5efd202c-e559-4645-a13e-c254b9c79da5). This is one of the reasons why web-applications have become popular: single pages can be referenced from e-mail, MS Word-documents, wikis and other web-applications like bug-management systems – and across the entire internet, for that matter.
Having a unique URL for each form and domain object instance is not supported by default in re-motion; the edit forms for all instances of a particular domain object class look alike. It is a natural consequence of using the economical Server.Transfer, but it flies into the face of idea that an URL provides an unambiguous reference to one particular page in the (inter)network. It is sometimes desirable to have the URL contain enough information to reconstruct the form the user sees, but this feature comes with some overhead.

Such an URL uniquely identifying a particular form (including all its input parameters) is called a "perma-URL". (Like a "permalink" to a blog-post or a link to a certain Google-query. You've probably sent around URLs like this occasionally.) This feature comes at the expense of performance, because Server.Transfer can't be used for such invocations, and Response.Redirect requires an extra round-trip (see FIXME for a backgrounder). Using perma-URLs for uniquely identifying forms for particular domain objects requires some modifications to the generated defaults, however.

Introducing call options, perma-URLs

Perma-URLs get their name from the fact that they keep their particular meaning forever (permanently), but in this light, the term is misleading. Some perma-URLs keep their meaning forever, but it requires extra work.
You can tell re-call to use perma-URLs when invoking the desired page/function. Remember (from the re-call section in the PhoneBook tutorial) that there are two overloaded Call methods for a WxePage. In the example for calling EditLocationForm upon New location..., we have used the simpler Call, the one without the args argument (two arguments). For invoking the EditLocationForm with options for a perma-URL, we must use the more elaborate three-argument Call. Here is an example. We create a call argument of type WxePermaUrlCallArguments and pass it to the EditLocationForm's Call method to signal that we insist on a perma-URL for the call.

public void LocationField_MenuItemClick(object sender, WebMenuItemClickEventArgs e)
{
  try
  {
    if (e.Item.ItemID == "NewLocation")
    {
      // NEW!
      var args = new WxePermaUrlCallArguments(true);
      // Note the extra "args" parameter
      var result = EditLocationForm.Call(WxePage, args, null);
      LocationField.Value = result;
    }
    else if (e.Item.ItemID == "SearchLocation")
    {
    }
  }
  catch (WxeUserCancelException) { }
}

[ The new code is not protected by an if (!IsReturningPostback) because it has no side-effects. What it does it can do twice without doing any harm. The only extra cost is that the invocation drains some (negligible) performance, but that's cheap for not messing up our code with an extra if. ]
For the called form, this code gives you a long URL with a WxeFunctionToken parameter for identifying the call stack built up to the point where the page in question is invoked. The problem with such WXE function tokens is that they are short-lived and will fall prey to the garbage collector eventually. WXE function tokens can't help much if you want to have your perma-URLs for longer – years, for example.

Let's talk about the resulting URL first. If you run this code and click on New location..., you will note that the URL for invoking the EditLocationForm has become much longer:


Now being invoked with a perma-url


... and here it is

The information transported by this parameter is also present if no perma-URL (i.e. a Server.Transfer) is used, but than it is invisible to the user.
The type of perma-URL we've programmed here sort of works, albeit not for very long. It only works for as long as the transaction and function is alive on the server. If you send the URL to a friend in the email, he or she will see what you see for as long as you have not committed the form and the referred function stack still exists on the server. Not very bright.

Why isn't "the domain object" included in the URL?

Generating and parsing meaningful URL-parameters for forms called with the form's Call method includes the arguments for the Call-method, under one condition: only values for arguments that can easily be serialized into the URL get a corresponding URL-parameter. Observe that this clearly rules out the Location object passed in obj. After all, it is hard to represent the property values of all its field into a URL.

You can see how URL-parameter generation works with a simple experiment. If you add an integer argument to the EditLocationForm function/page, you will see it show up as a URL-parameter, because an integer is easy to serialize – just write the number as a string. It's also easy to parse (with Int32.Parse(), for example). This only works with perma-URL representation turned on for a call of the EditLocationForm. Try this:

1.) add a new Int32-parameter named foo to EditLocationForm, so that the re-call header looks like this:

// <WxeFunction>
// <Parameter name="obj" type="Location" />
// <Parameter name="foo" type="Int32" required="false" /><!-- NEW! !-->
// <ReturnValue type="Location" />
// </WxeFunction>

[ We make the parameter optional with the required="false" attribute to avoid a mess in other spots of the web application. ]

2.) Add some integer to the EditLocationForm invocation in the event-handler for New location...:

var args = new WxePermaUrlCallArguments (true);
LocationField.Value = EditLocationForm.Call(WxePage, args, null, 42); // NEW: 42

3.) For completeness, add some integer to the SearchResultLocationForm.aspx.cs in the event-handler for Edit, LocationList_ListItemCommandClick:

EditLocationForm.Call (this, (Location)e.BusinessObject, 42); // NEW: 42

Your project should compile and link (but again: you have to build it twice to get rid of the errors). If you stimulate your PhoneBook web app into displaying the EditLocationForm, you will see the foo-parameter in the URL. You can either click on New Location... (EditPersonForm) or Edit (SearchResultLocationForm) (see screenshots).

Either way, your URL will display the utterly redundant (but instructive) foo=42.

Domain objects in the perma-URL

Let's return to our original proposition of sending around URLs that encode a pointer to one particular domain object, (a particular location, for example). It is not feasible to represent the domain object's entire set of values in an URL, but we can easily encode an object ID. Remember from the PhoneBook-tutorial (re-store part) that a complete ObjectID consists of

  • class information ("Location", for example)
  • some ID (at this time, only GUIDs are supported)
  • information on how the ID should be interpreted ("System.Guid")

The static method ObjectID.Parse can parse such a string representation in a format like Location|9388...E7|Guid. In contrast to the dummy foo-int from the previous exercise, having an object ID in the perma-URL actually makes sense: it enables users to link to a particular object.
So if we want to give each domain object in EditLocationForm|s its own unique URL, we do the following:

  • modify the signature of the Call method in such a fashion that the domain object's ID is passed rather than the .NET object itself
  • adapt the invocations to fit the new signature
  • for compatibility reasons, we should keep the current obj variable for holding the actual Location object
    [ Before working on this exercise, you should remove the instructive Int32 foo parameter from the EditLocationForm and restore the invocations to their original form. ]

Modify the re-call header for the object ID
The new header in EditLocationForm.aspx.cs shall look like this:

// <WxeFunction>
// <Parameter name="objid" type="ObjectID" />
// <ReturnValue type="Location" />
// <Variable name="obj" type="Location" />
// </WxeFunction>

We keep the obj as a local variable, so that the rest of the web-application remains compatible. However, me must take care that obj keeps its meaning by setting it in the edit form's Page_Load method. This method is also the spot where a new object is created if the objid parameter is null. Before our changes, Page_Load looked like this:

protected void Page_Load (object sender, EventArgs e)
{
  Title = ResourceManagerUtility.GetResourceManager(this) .GetString("Edit~Location");
  if (!IsPostBack)
  {
    if (obj == null)
    obj = Location.NewObject();
  }
  LoadObject (obj);
}
Take care that the new parameter is actually used

In the new and improved web application we want to set the obj page-local variable like this: obj = Location.GetObject(objid); (With objid being the new ObjectID page-parameter.)
Consequently, the new and improved Page_Load shall look like this:

protected void Page_Load (object sender, EventArgs e)
{
  Title = ResourceManagerUtility.GetResourceManager(this) .GetString("Edit~Location");
  if (!IsPostBack)
  {
    if (objid == null) // NEW! Improved!
    {
      obj = Location.NewObject();
    }
    // NEW! Improved!
    else
    {
      obj = Location.GetObject (objid);
    }
  }
  LoadObject (obj);
}
Change the EditLocationForm's Call invocation

At this point, your application will not build correctly, because the existing call to the EditLocationForm's Call method has become incompatible with the new state of affairs. The Call in EditPersonControl.aspx.cs (in the event handler for New location...) is uncritical and does not need to be changed. After all, just a null is passed as obj, or objid, so no modification is required. This invocation is also uninteresting, because New location... creates a new object instead of dealing with an existing one. To see how the improved URLs work, the Edit handler in SearchResultLocation.aspx.cs is much more instructive. The existing Call looks like this:

EditLocationForm.Call (this, args, (Location)e.BusinessObject);

However, we don't need the entire (Location)e.BusinessObject, just its ID:

EditLocationForm.Call (this, args, ((Location)e.BusinessObject).ID);

Consequently, the new LocationList_ListItemCommandClick handler shall look like this:

protected void LocationList_ListItemCommandClick (object sender, BocListItemCommandClickEventArgs e)
{
  if (e.Column.ItemID == "Edit")
  {
    try
    {
      var args = new WxePermaUrlCallArguments (true);
      EditLocationForm.Call (this,
                             args,
                             // NEW: ID 
                             ((Location)e.BusinessObject).ID);
      ClientTransaction.Current.Commit ();
    }
    catch (WxeUserCancelException)
    {
    }
  }
}
Try it out

As explained before, nothing interesting happens URL-wise when you create a new Location object with New location..., but clicking on the new object will give you a long URL. Here is what the author got from this demonstration:

http://localhost:22809/EditLocation.wxe?objid=Location%7ce2932471-1398-4c35-affa-2d3906da3288%7cSystem.Guid&WxeFunctionToken=5cdcb6f8-3953-42b8-8f31-23a740a5ed00&ReturnUrl=%2fSearchLocation.wxe%3fWxeReturnToSelf%3dTrue%26TabbedMenuSelection%3dLocationTab%252cSearchLocationTab