Persisting Ink on the Web

 

Julia Lerman
The Data Farm

October 2005

Applies to:
   Microsoft Windows XP Tablet PC Edition
   Windows XP Tablet PC Edition Development Kit 1.7

Summary: This article shows you how to move ink around in a Web site without falling prey to the many potential programming problems. Example code is in C# and JavaScript. (16 printed pages)

Contents

Introduction
The Four Persistence Methods
Moving the Persisted Data About Within a Web Site
Transferring with Hidden Values
Transferring Ink to another Ink-Enabled Control on the Same Page
Transferring Ink to an Ink Control on Another Page in a New Browser Window
Moving Ink to Another Page in the Same Browser Window
Transferring Ink to Another Page as a GIF
Storing Ink in an XML File on the Web Server to Be Used at a Later Time
Storing and Retrieving Ink from a SQL Server Database
Sending Ink to a Web Service
Surviving a Postback
ConclusionBiography

Introduction

The Tablet PC Platform's Ink API provides numerous ways for you to persist ink. However, when working with ink on a Web site you encounter limitations when interacting with ink, persisting ink, and moving ink from one Web page to another. This article shows you how to move ink around in a Web site without falling prey to the many potential programming problems.

The Four Persistence Methods

Once you have an Ink object that contains a Strokes collection, you can save the data into a byte array, formatted either as Ink Serialized Format (ISF) or GIF. The ISF contains all of the data from the Ink object and can be loaded back into an Ink object at any time. The GIF byte array is actually a fortified GIF file and contains two groups of data. The first is a byte array representing the drawing converted to a GIF. The second is the ISF. You can actually send a fortified GIF file to programs that can read a GIF and have no understanding of ISF as well as sending it to an Ink object, which will load the ISF portion of the file. For more information about the formats available for ink in HTML, see Storing Ink in HTML in the Tablet PC Platform SDK.

Both ISF and GIF are also paired with their Base64 encoded representations: Base64-ISF and Base64-fortified GIF. Base64 encoding is used to transport binary data where only ASCII data can go through. HTML is a perfect example of this, because HTML can't read binary data. Rather, it requires ASCII.

Even when you save ink with the Base64 formats, you are still returning a byte array and need to convert it to ASCII. Another way to achieve the same objective is to convert a regular ISF or Gif byte array to ASCII by using the ToBase64String() method. In this case it is not necessary to first use the Base64 persistence formats. The advantage of doing it this way is that you will have a smaller string to pass around.

The following table shows the difference in size of the byte array as well as the converted ASCII string for a very small ink drawing (just a few squiggled lines). If you are dealing with large amounts of data, differences like these are important to pay attention to.

Table 1. Byte Array and String Lengths of a sample Ink object persisted in the different formats.

  Byte Array length Base64 String length
GIF 1817 2424
Base64GIF 2436 3248
ISF 128 172
Base64ISF 188 252

Moving the Persisted Data About Within a Web Site

When working with ink-enabled controls on a Web site, remember that you do all of your interaction between the Web page and the control in client-side scripting. This is explained in detail in two previous articles on the Tablet PC Developer Center, Mobile Ink Jots 3: Ink on the Web and Doodling on the Web.

Because of this client-side scripting, we move the data around in HTML. Therefore, you must convert the byte array returned by the Ink.Save method to ASCII. This is similar in theory to the Base64 encoding discussed earlier. You need to go to the lowest common denominator for interaction. Therefore, for many of the procedures in the following examples, you first save the Ink object to a byte array (by using one of the four persistence formats) and then convert the byte array to a Base64-encoded string. Even the byte arrays that are already Base64-encoded still need to be converted to Base64-encoded ASCII.

The following sections note different ways of moving data around on a Web site and list the merits and shortcomings of each.

Transferring with Hidden Values

When working with the client-side functions, you can't fully take advantage of the ASP.NET Session object to move data from one page to another. You can take advantage of hidden fields, although it is important to remember that hidden fields are not secure. Their contents are visible in the source HTML of the page. You should never store any sensitive data such as signatures or social security numbers in a hidden field. The client-side script can interact with the hidden fields, reading from and writing to them. In the server side code—also known as the code behind—you can read the data from hidden fields as long as the hidden field's runat parameter is set to server. However, this doesn't work unless you also remove the form's runat=server parameter, which means that you won't be able to use most Web server controls in conjunction with this. You do have a few options for transferring data between pages, but note the special circumstances.

If PageA uses the script window.open to open PageB in a new window, then objects from PageA are available in the client-side code of PageB through the window.opener object. The following code (JavaScript) retrieves the value of the hidden field named MyHiddenField from the calling page.

Window.opener.document.forms("MyForm").MyHiddenField.Value

If PageA uses a hyperlink, form action submit, or a server transfer method, then the hidden field is available in the code behind by using the Request.GetValues(hiddenFieldName) method. This actually returns a string array, so to get at the actual value you will need to grab the first item in the array, as shown in the following (Visual Basic) sample.

'Creates an array object to store strings.
Dim MyStringArray() as String    
MyStringArray()=Request.GetValues("myHiddenFieldonPageA")
Dim ValuefromPageA as String=MyStringArray(0)

Once you have the data in the server-side code, you can do things like store it in the Session object to be used in other areas of your Web application, or even store the value back into another page's hidden field to be accessed by the client-side code. This is the method used in some of the following examples.

Now that you understand some of the limitations, take a look at some of the setup you must do in your ink control before you can start using the Web page techniques. If you have read the other Developer Center articles about ink on the Web, you already understand that the most efficient architecture is to let the ink control handle as much of its own functionality as possible. You will only be able to communicate with that control through client side script and need to expose that functionality.

The following samples (C#) are the methods used within the Windows Form control to expose the persistence functionality. inkO represents the InkOverlay object used to make the control ink-enabled.

Return Gif Persisted Ink as Base64 ASCII

   public string GetGIFasASCII()
  {
  // Be sure to return something, even if
  // there are no strokes at all.   
    if (inkO.Ink.Strokes.Count==0)
   {
       return "empty";
   }
    byte[] inkBytes=inkO.Ink.Save(PersistenceFormat.Gif);
    return Convert.ToBase64String(inkBytes);
  }

Return ISF Persisted Ink as Base64 ASCII

   public string GetISFasASCII()
   {
     if (inkO.Ink.Strokes.Count==0)
     return "empty";
     byte[] inkBytes =
            inkO.Ink.Save(PersistenceFormat.InkSerializedFormat);
       return Convert.ToBase64String(inkBytes);
   }

Return ISF Persisted Ink as a Byte Array

public byte[ ] GetISFasByteArray()
{
    if (inkO.Ink.Strokes.Count==0)
    return new byte[]{0};
    byte[] inkBytes=inkO.Ink.Save(PersistenceFormat.InkSerializedFormat);
    return inkBytes;
  }

Load from any type of Base64 ASCII persisted Ink

The Ink.Load method loads ink data no matter which persistence format you use. If you then convert the persisted data to a Base64 String, this method will be able to load it. In my own tests, I actually converted the color of the ink to red before loading it as a visual way to assure that I was, indeed, seeing the new ink.

public void LoadfromBase64ASCII(string ASCIIData)
{
   byte[] inkBytes=Convert.FromBase64String(ASCIIData);
   inkO.Ink.Load(inkBytes);
   this.Invalidate(); // Forces a redraw
}

Load from a Byte Array of persisted Ink

This method loads any persisted data that is still in the original byte array returned by the Ink.Save method, regardless of the persistence format used.

public void LoadfromInkBytes (byte[] InkData)
      {
      inkO.Ink.Load(InkData);
      this.Invalidate(); // Forces a redraw
      }

Some of the following scenarios can be achieved without any unnecessary extra roundtrips to the Web server; others do require some Web server interaction. In some cases more than one format may be possible, but the goal is to use the most efficient persistence format that gets the job done. Additionally, remember that it may not always be necessary to convert the returned byte array to text.

Note   Pay close attention to where HTML controls are used as opposed to Web controls.

These scenarios use the simplest method available but suggest advanced techniques for further exploration.

Transferring Ink to Another Ink-Enabled Control on the Same Page

In this case use the ISF format, because the data is used by another ink-enabled control. Additionally, the data does not need to survive a postback or be transmitted as HTML, which means that it is unnecessary to convert the byte array to Base64 ASCII text.

Two ink controls are embedded onto the Web page, along with an HTML button.

The following function (JavaScript) goes within the header script tags. This function gets the ink data from the first ink control and loads it into the second control.

function Dupe_ISFBytes()
 {
 bytes=Form1.inkControlA.GetISFasByteArray();
 Form1.inkControlB.LoadfromInkBytes (bytes);
 }

Transferring Ink to an Ink Control on Another Page in a New Browser Window

This method sends the actual binary data from one page to another, using pure JavaScript functionality. The advantage of this is that it enables server controls on the second page; the disadvantage is that it requires that you open the second page in a new browser window.

Because of the JavaScript functionality used in this method, you need not use a hidden field on the second page for moving data from the server to the client. Therefore, that page is still able to use server controls. To use JavaScript functions, however, you must open the second page in a new browser window. You still use a hidden control on the first page and script on the second page that forces the data to be loaded.

On the first page, embed an ink control. Next, drop a hidden input control onto the page and add runat=server to the <input> tag in the HTML. Note that you cannot do this through the control's property window. Then, add an HTML button to the page.

In the script at the top of the body tag, add a JavaScript function, InktoNewForm.

function InktoNewForm() 
{
Form1.InkDataBytes.value=Form1.inkControl.GetISFasByteArray();
window.open("Form2.aspx"); 
}

InktoNewForm gets the ink data as an ISF Byte Array, stores it into the hidden input field, and then opens up the next page by using the JavaScript window.open method. Opening the new page in a new browser window seems to be the only way to enable the new page to access the hidden value data purely from the client side.

In the client-side script of Form2, place the following script at the top of the body tag before the rest of the page is built.

   <script language= JavaScript>
   inkData=window.opener.document.forms("Form1").InkDataBytes.value
   </script>

This script grabs the data from the first page into a variable named inkData. Just before the closing </body> tag, insert another line of JavaScript to force the ink control to load the data that we put into the inkData variable.

   <script language= JavaScript>
   document.Form1.InkControl.LoadfromAnyInkBytes(inkData);
   </script>

You place this code just before the closing </body> tag because the call to the script must occur after the InkControl object renders.

This method uses no server-side resources and transfers the ink data as binary data, thus it is the most efficient method. It also allowed us to retain the form element's runat=server parameter, which means that if you need to include server controls to this page, you can.

Moving Ink to Another Page in the Same Browser Window

Another way to transfer ink data to a separate page is by storing the data as a Base64 String in a hidden field and moving it to the new page by way of the Web server. The downside of this method is that in order to share the hidden field data between the client and the server, the runat=server parameter must be removed from the form element. Again, the result of this is that you cannot use server controls on the page. The advantage of this approach is that a new page can be opened in the same browser window; the disadvantage of this approach is that a new page cannot have server controls.

The first page must have the embedded ink control, an HTML Submit button, and a hidden input field. Then:

  • The form must be set to submit to the second page.

    <form id="Form1" action="Form2.aspx" method="post">
    
  • The hidden field needs the runat=server parameter.

  • The button should have an OnClick parameter that calls the following JavaScript, placed in script tags just inside the <body> tag.

    function InktoNewForm() { Form1.InkDataAscii.value=Form1.inkControl.GetISFasASCII(); Form1.submit(); }
    

The script gets the ASCII representation of the ink data and stores it into the hidden field. Because the hidden field has the runat=server setting and that setting is removed from the Form tag, the contents are available from the next form.

Add the following code (C#) to the Page_Load method of the second page:

private void Page_Load(object sender, System.EventArgs e)
      {
            
         // Put user code to initialize the page here.
         string[] str;
         str=Request.Form.GetValues("InkDataAscii");
         this.InkDataAscii.Value=str[0];
         
      }

Then, get the client-side code to load that data into the ink control by using the following code (JavaScript):

if (!Form1.InkDataAscii.value=="")
 { Form1.inkControl.LoadfromAnyBase64ASCII(Form1.InkDataAscii.value);
 }

Put this code into script tags at the end of the body tags (between </form> and </body>) or turn this into a function that can be called from the same place.

Transferring Ink to Another Page as a GIF

This method uses similar techniques to the previous one but with a few differences. Because you output a GIF, you need to persist the ink data by using the control's GetGIFasASCII method. This returns the GIF format as Base64 Encoded ASCII.

Transfer the data to the next page by using the Request.Form.GetValues() method, as in the previous example. However, on this page, you do not use an embedded ink control, because your output is a standard graphic image. Instead, convert the ASCII back into the original byte array and then use the Response.BinaryWrite method to output the image to the page, as in the following example (C#).

string[] str;
   str=Request.Form.GetValues("InkDataAscii");
   byte[] inkBytes=Convert.FromBase64String(obj.ToString());
   Response.BinaryWrite(inkBytes);

Note that the BinaryWrite method displays only the image on the web browser page, replacing any HTML. Doodling on the Web, on the Tablet PC Developer Center, demonstrates how to output the image to a page that is the source of an image tag on another page. This way, the image can be incorporated into a fully designed page.

Storing Ink in an XML File on the Web Server to Be Used at a Later Time

For this function you truly want to leverage the ASP.NET server-side code. Remember that the window's control cannot write a file to the Web server, because the control is on the client computer. Although the most familiar method may be to create an aspx page with nothing on the UI and do all of your work in the code behind, this means implementing the entire page object model, which is fairly inefficient. All you really need here is the server-side code and access to the HttpRequest and HttpResponse classes so that you can interact with the calling page. The best means of accomplishing this is through the IHttpHandler interface. IHttpHandler is an interface that you can easily implement in a class. You can then integrate that class with a file type called ashx and call the class just as you would call an aspx Web page. To do so, create an ashx file with nothing but a page directive tag which points to the class that implements the IHttpHandler interface (implementation details to follow).

Use the StoreXML class (C#) for saving an ink drawing into a file on the Web server. Note that the class implements IHttpHandler, giving it access to the HttpResponse and HttpRequest objects. The StoreXML class also implements the IRequiresSessionState interface in order to access the Session object.

using System;
using System.Web;
using System.Web.SessionState;
using System.IO;
using System.Xml;


public class StoreXML: System.Web.IHttpHandler,IRequiresSessionState
{
   public void ProcessRequest (HttpContext context)
   {         
      
      string[] asciistr;
      // Create and save the xml file in the code behind, 
      // not in the window's control.
      // Remember, the window's control is on the client computer.
      asciistr=context.Request.Form.GetValues("InkDataASCII");
      // Create a new file.
      FileStream fs=new FileStream(context.Server.MapPath("") + 
            "\\inkdataasxml.xml",System.IO.FileMode.Create);
      // Create xmlTextWriter to write data into the file.
      XmlTextWriter xwriter = new XmlTextWriter(fs, 
            System.Text.Encoding.UTF8);
      // Create the starting tag.
      xwriter.WriteStartDocument();  
      xwriter.WriteStartElement("picture");
      // Write ink data into the file.
      xwriter.WriteElementString("Ink", asciistr[0]);  
      xwriter.WriteElementString("Author","Julie");
      xwriter.WriteEndElement();
      xwriter.Close();
      // Store ink data into the session for next page to retrieve.
      context.Session["ReturnInkData"]=asciistr[0]; 
      context.Server.Transfer(context.Request.UrlReferrer.LocalPath);
      }
   
   
   public bool IsReusable
   {
      get {return true;}
   }

}

Here is a sample of the xml file that is created.

<?xml version="1.0" encoding="utf-8"?>
<picture>
<Ink>AGEdBJoB+AEDBEgQRTUZFDIIAIAQAgAAKEIzCACADAIAAChCEauq00EKOCqD+jPpPNS4D
PHlKoikTFds85xHO8xMRmKiQITzJ5VpDJTbhxwkrHDjz13ZpQspeM9OOlKKRtGA</Ink>
<Author>Julie</Author>
</picture>

Note that the ink data is stored in the ASP.NET Session object. This is the trick for getting the ink back onto the first page when you return to it.

Now that you created the HttpHandler, create an ashx file that can be called from the previous page. Microsoft Visual Studio .NET 2003 does not have a built-in shortcut for creating an ashx file. Instead, you can just create a new code file item, which will be an empty file, and then change its extension to ashx. On Visual Studio .NET 2003, click the Project menu, click Add Component. In the Templates pane, select Code File, and then click Open. In Solution Explorer, right click CodeFile1.cs, click Rename, and then rename the file with the .ashx extension.

The only element you need in this file is the page directive that points to the StoreXML class that you created earlier. If the class were inside of a namespace, you would use its full name in the Class parameter.

<%@ WebHandler Language="C#" Class="StoreXML" %>

Note that Visual Studio .NET 2003 has very little documentation about the ashx format, although it does mention that the format is supported. Look for further documentation in Visual Studio .NET 2005, or on other, third-party Web sites. Another method is to send the data to a Web service, explained later in this article.

After you write the code to store the XML, call that code from the page that is collecting the ink. In addition, prepare that page to redisplay the original ink that you will add to the Session object. To do so, you need your embedded ink control, an HTML button, and a hidden input field with the runat=server parameter set on the Web page.

In the client-side script, add the following function:

function InktoXMLData()
 {
Form1.InkDataASCII.value=Form1.inkControl. GetISFasASCII ();
  if (Form1.InkDataASCII.value!="empty")
     Form1.submit();
}

The Form1.submit() method depends on the form's action parameter pointing to the ashx file.

In the Page_Load method of the original page, the following code (C#) grabs the ink data from the Session object and stores it back into the hidden input field, making the data available to the client-side code.

private void Page_Load(object sender, System.EventArgs e)
{
   if(Session["ReturnInkData"]!=null)
   this.InkDataASCII.Value=((string)Session["ReturnInkData"]);
}

You then use the same JavaScript method shown in the earlier examples to load the ink into the ink control after the page has rendered.

Storing and Retrieving Ink from a SQL Server Database

You can rely on the same technique used for creating an XML file to bridge the gap between the client-side ink data and processes such as interacting with a database or even communicating with a Web.

As with the StoreXML example, create a class that implements IHttpHandler to retrieve the ASCII formatted ink data from the page on which it was drawn. From there, depending on your application's architecture, either save the ASCII data directly to your database as a char or pass it on to your data layer for database updates.

The following sample (C#) shows a class that implements IHttpHandler and stores ink data into a SQL Server database.

using System;
using System.Web;
using System.Web.SessionState;
using System.Data.SqlClient;

public class StoreinDB: System.Web.IHttpHandler,IRequiresSessionState
{
   public void ProcessRequest (HttpContext context)
   {         
      string[] asciistr;
      asciistr=context.Request.Form.GetValues("InkDataASCII");
      // Best practice is to store your connection elsewhere and use
      // stored procedures when you can.
      SqlConnection conn=new 
            SqlConnection("server=TABBY;Trusted_Connection=True;Database=pubs");
      SqlCommand comm=new SqlCommand("INSERT INTO inkdata(chardata) 
            VALUES('" + asciistr[0]  + "' )",conn);
      conn.Open();
      comm.ExecuteNonQuery();
      conn.Close();
      // Store the ink data into session for the next page to retrieve.
      context.Session["ReturnInkData"]=asciistr[0];
      context.Server.Transfer(context.Request.UrlReferrer.LocalPath);
      }
   public bool IsReusable
   {
      get {return true;}
   }

}

The StoreinDB.ashx file contains the following:

<%@ WebHandler Language="C#" Class="StoreinDB" %>

The calling page uses GetDB.ashx as the target of the form's submit parameter.

Use the ink control's GetGIFasBase64ASCII() method to get the best format for storing the ink data.

Be considerate of the datatype of the field in your database table where you store the data. The varchar datatype takes ASCII data with a variable length up to 8000 characters while the text datatype, also variable, can be much larger. Also, the benefit of using varchar and text over char is that even if you have the length of the varchar field set to 8000, it only grows to the size of the actual data stored in the field.

The following example (C#) shows another class that implements IHttpHandler. This class retrieves this same stored data from the database and then places it into the Session so that it can be displayed on the form making the call. The ID necessary for retrieving the proper row is stored in a hidden field on the calling form.

using System;
using System.Web;
using System.Web.SessionState;
using System.Data.SqlClient;


public class GetDB: System.Web.IHttpHandler,IRequiresSessionState
{
   public void ProcessRequest (HttpContext context)
   {         
     string drawingID;
     drawingID=Request.Form.GetValues("DrawingID")[0];
     object obj;
// Best practice is to store your connection string elsewhere and 
// use stored procedures when possible.
     SqlConnection conn=new 
         SqlConnection("server=TABBY;Trusted_Connection=True;Database=pubs");
     SqlCommand comm=new SqlCommand("select chardata from inkdata 
            where drawingid=" + drawingID,conn);
     conn.Open();
     // Get the first column of the first record in the query.
     obj= comm.ExecuteScalar(); 
     conn.Close();
     context.Session["ReturnInkData"]=obj.ToString();
     context.Server.Transfer(context.Request.UrlReferrer.LocalPath);
   }
public bool IsReusable
   {
      get {return true;}
   }
}

If you persist the ink as GIF rather than ISF, you can then stream the image data out of the database by using a variety of methods. Just remember that most of these methods require that you convert the ASCII back into a byte array before working with it.

The following example (C#) gets an image from the database and streams it back to the Web browser. This is similar to Transferring Ink to Another Page as a GIF.

   object obj;
   SqlConnection conn=new 
            SqlConnection("server=local;Trusted_Connection=True;Database=pubs");
   SqlCommand comm=new SqlCommand("select chardata from 
            inkdata where drawingid=6",conn);
   conn.Open();
   obj= comm.ExecuteScalar();
   conn.Close();
   byte[] inkBytes=Convert.FromBase64String(obj.ToString());
   Response.BinaryWrite(inkBytes);

For more information about using the FileStream object and a byte array in ASP.NET to write binary large object (BLOB) data to Microsoft SQL Server, see HOW TO: Read and Write BLOB Data by Using ADO.NET Through ASP.NET.

Sending Ink to a Web Service

Although there are ways to call XML Web services from JavaScript, you can also do this by using a class that implements IHttpHandler, calling the Web service by using the .NET Framework APIs. The following sample (C#) sends the ASCII data on to a Web service, which is designed to store that data into a database.

   string[] asciistr;
   bool returnval;
   localhost1.InkServices inkService=new localhost1.InkServices();
   asciistr=context.Request.Form.GetValues("InkDataASCII");
   returnval=inkService.StoreinDB(asciistr[0]);
   context.Session["ReturnInkData"]=asciistr[0];
   context.Server.Transfer(Request.UrlReferrer.LocalPath);

For more information about calling Web services from JavaScript by using WebService behavior, see Using the WebService Behavior in the MSDN Library.

Surviving a Postback

It is tricky to get ink data to survive a postback. After much trial and error, I discovered the following solution maximizes the chances of your ink surviving a postback. Remember, this is not recommended for any ink data that you need to keep secured.

This method involves using an ASP.NET server-side TextBox control that is hidden. Note that this does not work with a Label control. The TextBox control is enabled to be included in the page's viewstate by default.

First, ensure that when the page is submitted, the ink data is stored into the TextBox. This needs to happen on the client side, where you have access to the ink data.

Use a JavaScript function for this purpose, one that calls the ink control's function to return the ISF as a String.

function Store_ISFAscii()
 { document.all('TextBox1').value=Form1.inkControl.GetISFasASCII();
 }

You then force the ink to be loaded. Here, the script is in its own function, PageLoadInk, which gets called at the end of the <body> tag. However, on this page, the function loads from the TextBox.

   function PageLoadInk()
   {
   inkdata=document.all('TextBox1').value;
   if (inkdata!="")
   Form1.inkControl.LoadfromAnyBase64ASCII(inkdata);
   }

Conclusion

It seems like a small miracle to make your Web applications ink-enabled, but there are many considerations of what to do with the ink once it has been drawn. This article has explained the many complexities of persisting ink data when creating an ink-enabled ASP.NET Web site. These solutions should help with some of the most common scenarios:

  • Moving ink around a Web site.
  • Storing ink on a Web page and a server.
  • Refreshing ink on a Web site.

The article also presents some advanced topics, such as the use if the IHttpHandler interface and storing image data in SQL Server. There is a lot more to explore with the possibilities of using ink on the Web. It's enough to keep the imagination flowing!

Biography

Julia Lerman is an independent consultant who has been designing and writing software applications for 20 years. She lives in Vermont where she runs the Vermont.NET User Group. Julia is well known in the .NET community as an INETA Board member, .NET MVP, ASPInsider, conference speaker, and prolific blogger. You can read Julia's blog at thedatafarm.com/blog.