Smart Clients

Build A Windows Forms Control To Consume And Render WSRP Portlets

Carl Nolan

This article discusses:

  • Understanding WSRP interactions
  • Building a WSRP viewer control
  • Security considerations for WSRP clients
This article uses the following technologies:
.NET Framework, Web Services, Windows Forms

Code download available at:WSRP.exe(317 KB)

Contents

The WSRP Makeup
WSRP Viewer Architecture
Establishing Relationships and Capabilities
Obtaining and Rendering Markup
Processing Interactions
State Persistence and Destruction of Relationships
Viewer Security
Wrap-Up

Today's organizations need sites that deliver interactive functionality and data in a secure environment. Building them could translate into a lot of development time, if you were to start from scratch. The portlet—a form of distributed Web component—simplifies this task.

When working with portlets, well-defined interfaces are essential; they allow you to easily integrate portlets into Web applications. The Web Services for Remote Portlets (WSRP) standard defines such an interface, allowing the aggregation of remote, interactive Web service-based presentation components. WSRP provides the means for interactive content to be produced and then consumed by portal applications, with minimal programming effort. The portal then acts as an intermediary between the user and the aggregated services provided by the producer.

WSRP represents a noble goal, but there are often better alternatives. Web services that pass around data rather than UI constructs are more flexible, and Visual Studio®, in concert with the Microsoft® .NET Framework, makes quick work of consuming such services and supplying quality UI. That said, if you need to work with a service that is already providing interactive Web components as WSRP portlets, you can incorporate these portlets seamlessly into a smart client application. This is appealing because smart client applications use local resources, provide a rich client experience, and support intelligent install and update mechanisms. Web services offer powerful interoperability and application integration features. Combining the two makes it possible to develop integrated applications that can incorporate data from disconnected sources.

To really understand portlets it helps to take a look at a Windows® Forms control that implements the WSRP specifications—consuming third-party portlets exposed as WSRP compliant Web services. In this article, I will walk you through the implementation of just such a control and the classes used to implement the WSRP standard (the code for this control is available from the MSDN®Magazine Web site).

The WSRP Makeup

WSRP defines a set of APIs that allows applications to utilize remote interactive Web services in the form of portlets. These APIs are built on top of the existing SOAP, WSDL, and UDDI Web services technologies.

The WSRP specification describes the conversation between producers and consumers on behalf of the users. These producers are the presentation-oriented Web services that host portlets, rendering the markup and processing user interactions. The consumers are the applications that integrate these Web services, presenting markup to users and managing user interaction.

WSRP provides a set of standard operations that enable a consumer to request valid markup fragments (such as HTML) from a producer based on an existing state. Users can then interact with the markup, while the state is preserved across these calls. Figure 1 and Figure 2 show the key operations and types that will be consumed by my WSRP viewer control and service class. This is not a definitive set, but represents the key elements required.

Figure 2 WSRP Types

Type Description
ServiceDescription The return type from a getServiceDescription call, containing the service metadata.
RegistrationContext Type used to pass the registration state and handle to the WSRP producer. Also the return type from a WSRP register request.
PortletDescription Type describing the portlets. Array of offered portlets contained within the ServiceDescription type.
RegistrationData Type used to pass the fields needed by the WSRP producer to return the registration context.
UserContext Type used to pass the user information to the WSRP producer.
SessionContext Type used to pass the session information to the WSRP producer.
RuntimeContext Type used to pass the runtime information to the WSRP producer.
PortletContext Type used to pass the portlet information to the WSRP producer. Includes the portlet handle and interactive state.
MarkupParams Type used to pass the set of fields needed by the WSRP producer to generate markup for the user.
MarkupResponse Type containing the fields returned from a WSRP getMarkup request.
MarkupContext Type containing the fields from a markup invocation, including the actual markup. Contained within the MarkupResponse type.
InteractionParams Type used to pass the set of fields needed by the WSRP producer when performing a user interaction.
BlockingInteractionResponse Type containing the fields returned from a WSRP performBlockingInteraction request.
UpdateResponse Type containing the fields from an interaction invocation. Contained within the BlockingInteractionReponse type.
UploadContext Type containing the fields specific to uploading data to the portlet.

Figure 1 WSRP Operations

Operation Description
getServiceDescription Acquires the WSRP producer's metadata.
Register Establishes a relationship with a WSRP producer.
modifyRegistration Modifies a relationship with a WSRP producer.
initCookie Allows the consumer to assist the WSRP producer in creating cookies.
clonePortlet Allows the consumer to request the creation of a new portlet from an existing one.
getMarkup Requests the markup for rendering the current state of a portlet.
performBlockingInteraction Allows for the potential modification of the state of a portlet. Initiated through an end user interaction.
Deregister Destroys a relationship with a WSRP producer.
releaseSessions Informs the WSRP producer that a set of sessions are no longer required.
destroyPortlets Allows the destruction of portlets no longer required by the consumer.

When the WSRP viewer control is displayed, the user can select from a list of available portlets through the toolbar. This interaction creates relationships with the WSRP producer and determines its capabilities. Next, the markup, which contains action links that the user can interact with, is acquired and rendered. When the user interacts with the action links, a blocking interaction takes place. This modifies the state of the portlet. Then new markup is acquired and rendered. Finally, once the user interaction is complete, all previous relationships are terminated. The interaction flow of my smart client WSRP viewer is illustrated in Figure 3.

Figure 3 Interaction Flow in a WSRP Viewer

WSRP Viewer Architecture

Before delving into WSRP, I'll outline the principles that have guided my implementation. I set out to create a Windows Forms control that would encapsulate the interaction with a WSRP producer. This would include all the required visual elements that allow the user to interact with the WSRP portlets. As part of this effort, I've created a reusable WSRP class library, which is consumed by the Windows Forms control to encapsulate all schematics of calling WSRP-compliant Web services.

The WSRP Viewer will demonstrate the loading and displaying of the HTML markup by using the appropriate Web browser control and HTML DOM library. WSRP calls will be transported over HTTP and will support HTML markup only.

I will also demonstrate how to handle the scope of the WSRP sessions through session and registration calls. This includes defining user-defined registration properties. Finally, I will demonstrate how the WSRP Viewer can provide offline storage of the registration and portlet state for later reuse.

Figure 4 illustrates a high-level design model that encompasses the main components of the WSRP Viewer architecture. It includes the Windows Forms viewer control, the WSRP service class, and the supporting class libraries. Other components are present, but as helper and event delegate classes.

Figure 4 WSRP Viewer Design Model

Figure 4** WSRP Viewer Design Model **

WsrpViewerControl is the Windows Forms control. This component's primary functions are to present HTML (rendering markup and capturing HTML events for processing), handle user interaction, and initiate WSRP state changes. The graphical elements handle all the required user interactions: connecting to a predefined WSRP producer, portlet selection, and transitions between WSRP modes and views. Figure 5 shows the viewer control when connected to a document management portlet.

Figure 5 Rendering a Document Management Portlet

Figure 5** Rendering a Document Management Portlet **

The WsrpService class is the wrapper for the WSRP Web service requests, and represents an implementation of the WSRP specification. The PersistenceState type, through XML serialization and deserialization, will persist state and configuration data between invocations of the service class. This type aggregates registration and portlet state information and is persisted through the use of the PersistenceStateStorage class.

The WsrpServiceUrl class is a simple wrapper for the four required WSRP URLs: service description, markup operations, registration, and portlet management. Each instance of the WsrpService class is tied to a specific URL endpoint and is therefore defined by this property during an initialization call. This component also supports a type converter derived from ExpandableObjectConverter to allow for integration into the Visual Studio properties window.

The code base for the WSRP Viewer is too large to cover in a single article. Therefore, I will break down the WSRP processing into its essential elements, outlining how the WsrpService class handles these interactions and how the WsrpViewerControl interacts with the service. Later, I will discuss security and exceptions.

Establishing Relationships and Capabilities

Before you can interact with a WSRP producer, you need to establish a relationship with the producer and determine its capabilities. This is achieved through the WSRP getServiceDescription and register operations, which are wrapped within the service class InitializeService method. During this initialization, the operating mode of the WSRP interactions is defined (through an enumeration called WsrpOperatingMode) as one of the values shown in Figure 6.

Figure 6 WsrpOperatingMode Values

Value Description
ModifyGlobalState Support global changes and no portlet management.
UsePortletManagement Support user state persistence and portlet cloning through the portlet management interfaces, if supported.
Stateless Support user state persistence but no portlet management cloning; the WSRP producer can, however, create a portlet clone for the user during an interaction. This is the default state.

The operating mode parameter is important because it determines the scope of portlet state changes. Internally, two properties are defined: SupportGlobalChanges and SupportPortletManagement.

The SupportGlobalChanges flag determines whether changes are made to the global portlet state or individual user clones. By default all portlet state changes are per user and do not affect the global state. Setting this to true, however, will allow modifications to the global portlet state.

The SupportPortletManagement flag determines the portlet cloning method used. If the portlet has been enabled for user-specific state (the WSRP hasUserSpecificState indicator set to true), then a WSRP clonePortlet request will be made for the user. Otherwise, the WSRP cloneBeforeWrite indicator will be used on the WSRP performBlockingInteraction requests (the default mode).

The SupportPersistence property also affects the portlet state operation. This flag, which defaults to true, determines whether the previous registration and portlet state information is initialized from a previous interaction. This defines a value for the internal RegistrationPstate and PortletPstate array properties.

The main purpose of the InitializeService method is to acquire registration data and determine the producer's capabilities. This includes determining the list of portlets with which the user can interact. Therefore, once called, the URL defining the interaction becomes immutable. The core code for this is in Figure 7.

Figure 7 Registration Data and Capabilities

// make the initial service description call with blank registration _serviceDescription = wsrpGetServiceDescription(); // is required for the WSRP producer? make call again _usesRegistration = _serviceDescription.requiresRegistration; if (AcquireRegistration()) _serviceDescription = wsrpGetServiceDescription(); // iterate through the portlets; persist the portlet title and state _portletDescriptions = _serviceDescription.offeredPortlets; if (_portletDescriptions.Length > 0) { _portletTitles = new string[_portletDescriptions.Length]; _portletPstates = new PortletPstate[_portletDescriptions.Length]; for (int idx = 0; idx <_portletDescriptions.Length; idx++) { _portletTitles[idx] = _portletDescriptions[idx].title.value; _portletPstates[idx] = new PortletPstate(_portletDescriptions[idx].portletHandle); } } // now look to see if cookie assistance is required if (_serviceDescription.requiresInitCookie != CookieProtocol.none) { // set up the collection for the cookies for markup calls _cookieCollection = new Hashtable(); _usesCookies = true; } else { // ensure markup has a cookie collection although not persisted _wsrpMarkup.CookieContainer = new CookieContainer(); _cookieCollection = null; _usesCookies = false; }

When a user registers with a WSRP producer, a registration context is returned. This registration context must be used on subsequent markup and interaction calls. If a WSRP producer requires registration, the service description request must be made again with the new registration context. This actual registration process is wrapped in the AcquireRegistration method. The core code is shown here:

RegistrationContext registrationContext = wsrpRegister(); if (registrationContext != null) { _registrationPstate.RegistrationHandle = registrationContext.registrationHandle; _registrationPstate.RegistrationState = registrationContext.registrationState; }

All the portlet information is persisted in an array of PortletDescription types. While processing the markup and interactions, this information (along with the user portlet state information) is used to determine the exact markup that is rendered.

Since the producer can ask for assistance with cookies, the final process for the service description is to determine the cookie requirements. At this point, the initialization process merely defines a holder for the cookie containers. The container contents are then defined and used during WSRP getMarkup and performBlockingInteraction requests. The service maintains a table of CookieContainers based on the GroupId of the portlet or the calling user. The final determination of which container is to be used is based on the producer's requirements and the portlet specifications. When a new cookie is required, the WSRP initCookie request is made and the cookie is added to the collection (see Figure 8).

Figure 8 Initializing Cookie Groups

string cookieGroup; if (_serviceDescription.requiresInitCookie == CookieProtocol.perUser || _portletDescriptions[_portletIndex].groupID == null) cookieGroup = _callingUser; else cookieGroup = _portletDescriptions[_portletIndex].groupID; // see if the cookie container has been defined if (_cookieCollection.ContainsKey(cookieGroup)) { // return the previously defined cookie container cookie = (CookieContainer)_cookieCollection[cookieGroup]; } else { // cookie not in the collection, so create and add into // the cookie group cookie = wsrpInitCookie(); _cookieCollection.Add(cookieGroup, cookie); }

The WSRP producer relationship is not automatically established when the viewer control is initially displayed. Since the service URL can be defined either programmatically or within the designer, this relationship is not established until requested either programmatically or through a user interaction.

Figure 9 WSRP Connection Settings

Figure 9** WSRP Connection Settings **

To assist the initialization process, the viewer control supports a connect toolbar and context menu item. Both of these display the WSRP Producer Connect dialog box shown in Figure 9. This allows the user to select a portlet and initial state. Upon rendering the dialog, an instance of the WsrpService class is constructed and the InitializeService method is executed, making the WSRP getServiceDescription request. The control properties are used to determine the operating mode and whether the previous state information requires rehydration.

Although a toolbar and context menu is integral, portlet initialization can also be managed programmatically, as shown here:

wsrpViewerControl.WsrpInitializeProducer(); wsrpViewerControl.WsrpPortletHandle = "781F3EE5-22DF-4ef9-9664-F5FC759065DB";

Code initialization offers the additional advantage that the initial portlet can be selected through the PortletHandle property. This property is the handle assigned to the portlet by the WSRP producer. To find its value, the appropriate portlet can be selected manually and the handle property inspected and persisted offline. Using this technique, you can predetermine the portlet that is displayed to the user.

Obtaining and Rendering Markup

Once a relationship to the WSRP service has been established, you can obtain the required portlet markup. However, you need to determine whether a portlet requires cloning prior to making markup calls. This determination is based on whether the portlet supports clones and the operating mode, after which a WSRP clonePortlet request is made:

if (_portletClones && _supportManagement && !_supportGlobal) { PortletContext portletContext = wsrpClonePortlet(); _portletPstates[_portletIndex].CloneHandle = portletContext.portletHandle; _portletPstates[_portletIndex].PortletState = portletContext.portletState; }

Upon making the WSRP clonePortlet request, the returned clone handle is persisted and used for all subsequent calls. To support the persistence of clone handles, the PortletPstate type defines two portlet handles: one for the original and one for the clone. PortletPstate also supports a CurrentHandle method to determine the appropriate handle for interactions.

A WSRP getMarkup request is used to obtain the HTML markup and corresponding markup title, passing in the requested portlet handle and state information. The core code for making the request is shown in Figure 10.

Figure 10 Retrieving HTML Markup

getMarkup getMarkupParam = new getMarkup(); getMarkupParam.registrationContext = GetRegistrationContext(); getMarkupParam.markupParams = GetMarkupParams(); getMarkupParam.portletContext = GetPortletContext(); getMarkupParam.runtimeContext = GetRuntimeContext(); getMarkupParam.userContext = GetUserContext(); if (_usesCookies) _wsrpMarkup.CookieContainer = GetCookieContainer(); // make the WSRP get markup call MarkupResponse markupResponse = _wsrpMarkup.getMarkup(getMarkupParam); // after successful call ensure parameters are persisted if (_usesCookies) SetCookieContainer(_wsrpMarkup.CookieContainer); _sessionContext = markupResponse.sessionContext; // Extract the markup and perform post processing MarkupContext markupContext = markupResponse.markupContext; ExtractMarkup(markupContext, out html, out title); if (markupContext.requiresUrlRewriting) html = ProcessWsrpRewrite(html);

When defining the parameters for the WSRP getMarkup request, there are several WSRP types that must be defined: RegistrationContext, PortletContext, RuntimeContext, UserContext, MarkupParams, and CookieContainer. The MarkupParams type defines the parameters, determining the markup that will be returned. MarkupParams includes fields for locale information, MIME types, allowed character sets, WSRP mode, window state, and the navigational state. The WSRP mode and window state are those selected by the user through the viewer control—although, they can be changed when performing an interaction.

Initially, the navigational state is undefined, but is then modified during interactions. A new value is derived from an action URL or as a return value from a blocking interaction request. Between instances of the service class, the navigational state (along with other state information) is persisted, allowing the initial markup request to return markup for a previous state condition.

Exception handling is an important process during markup acquisition. When a WSRP getMarkup request is made, there are a number of exceptions that warrant further processing. These include invalid cookies, session information, portlet clone handle, and registration information. Through analysis of the request exception, you can potentially handle each of these failures and reprocess the WSRP getMarkup request (see Figure 11).

Figure 11 Analyze and Reprocess Requests

if (ex.WsrpFaultCode == WSRP_FAULTCODE_INVALIDCOOKIE) { // clear out the cookie and try the call again reprocess = ReleaseCookie(); } else if (ex.WsrpFaultCode == WSRP_FAULTCODE_INVALIDSESSION) { // release the session and try again reprocess = ReleaseSession(); } else if (ex.WsrpFaultCode == WSRP_FAULTCODE_INVALIDHANDLE) { // see if using clone and if so ensure blanked out and try again if (!_portletPstates[_portletIndex].IsCloneEmpty) { _portletPstates[_portletIndex].CloneHandle = null; AcquireClonePortlet(); reprocess = true; } } else if (ex.WsrpFaultCode == WSRP_FAULTCODE_INVALIDREGISTRATION) { // blank out any registration data to make the call again if (!_registrationPstate.IsEmpty && !_usesRegistration) { // have data and should not so reprocess _registrationPstate = new RegistrationPstate(); reprocess = true; } else { // try and acquire new registration data if needed reprocess = AcquireRegistration(); } }

Finally, your WSRP consumer must process the markup URLs. URL rewriting within a Windows Forms control is simplified since there is no intermediate consumer agent. The service class, within the ProcessWsrpRewrite method, takes the simple approach of replacing the following string values with an empty value:

wsrp_rewrite? /wsrp_rewrite wsrp-urlType=resource&wsrp-url= wsrp-urlType=resource&wsrp-requiresRewrite=true&wsrp-url=

This simple approach works for a smart client application since the viewer control captures all events and a URL is not required. However, there are some caveats to consider. This approach will not work for a portal consumer application, and some complex routines are needed to complete this process.

When returning the HTML markup and corresponding title, the viewer control needs to render the information. The title is a viewer control property that is displayed within the toolbar. The HTML markup is displayed as the innerHTML of the Microsoft Web Browser control.

At this point, I should take a moment to discuss the HTML rendering process. To have access to the HTML DOM (the mshtml type library), the Web browser must first load an appropriate document. To achieve this, the viewer control navigates to the "about:blank" document and, in effect, loads the HTML DOM. From this point onwards all markup is rendered by modifying the document body innerHTML property.

You direct the Web browser to a URL through its Navigate method. Navigation completion is indicated by the document complete event. To obtain the HTML document, the Web browser Document property is cast to an mshtml.HTMLDocument interface object. This object can be queried for its properties and collections as well as to access the HTML body (supporting the mshtml.HtmlBody interface) so as to manipulate the HTML DOM:

document = (mshtml.HTMLDocument)this.webBrowser.Document; body = (mshtml.HTMLBody)document.body;

Don't assume this is a straightforward process, however. There are some complexities associated with URL fixing due to the use of "about:blank" navigation. This involves some COM interop. Similar processing is required to suppress the default context menu of the Web browser and instead display one more suitable to WSRP processing.

The BeforeNavigate2 event, exposed by the Web browser control, handles markup interactions. It allows you to capture, cancel, and internally process any navigation event. This is necessary within a WSRP viewer control because any interactions require processing through a WSRP call, with the parameters determined from the navigation URL. The DocumentComplete event captures the initial "about:blank" navigation to perform post-navigation processing.

Note that when processing the HTML markup, there may be tags present that the HTML specification specifically prohibits from occurring outside the head element of the document. The actual markup fragment returned from the producer can contain any number of elements, but it cannot use body, frame, frameset, head, html, or title tags. Other tags will require additional processing, including the script, style, and link tags. If these tags are not managed separately, their values will be lost when translated into the innerHTML property.

Script tags and style tags are handled in a similar fashion. The corresponding DOM element is created using the markup values. These Elements are then appended into the body of the document. Some special processing is required for link tags since the contents of a linked stylesheet can contain URLs that require rewriting. Therefore, any stylesheets referenced within a link tag are retrieved, processed, and cached in memory for inclusion into the rendered markup. In a standard portal consuming app, these resources—including referenced images—are cached on the consumer Web site and the URL is rewritten to point to the consuming portal.

This processing of special tags is encapsulated with the SetTextMetadata method, shown in Figure 12. Regular expressions are used extensively here to parse the returned markup for the required tags. A helper method, GetResourceString, collects the linked stylesheet resources and caches them. This optimizes performance because the stylesheets may be required for rendering markup in a portlet.

Figure 12 HTML Tag Processing

private void SetTextMetadata(string html) { // define source variables string scriptSource = null, scriptTag = null, styleSource = null; string styleTag = null, linkTag = null; // extract any script and style tags for later processing if (html != string.Empty) { // see if have a link tag for processing Regex linkExpression = new Regex(LINK_PARSE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture); Match linkMatch = linkExpression.Match(html); // see if a match was found and if so process if (linkMatch.Success) { linkTag = linkMatch.Result(LINK_TAG_PARSE_MATCH); } // see if a style tag has been included in the return if (Regex.IsMatch(html, STYLE_PARSE_PRE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline)) { Regex styleExpression = new Regex(STYLE_PARSE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture); Match styleMatch = styleExpression.Match(html); // see if a match was found and extract the script if (styleMatch.Success) { styleTag = styleMatch.Result(STYLE_TAG_PARSE_MATCH); styleSource = styleMatch.Result(STYLE_SOURCE_PARSE_MATCH); } } // see if a script tag has been included in the return if (Regex.IsMatch(html, SCRIPT_PARSE_PRE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline)) { // define a regular expression for the Html Body parsing Regex scriptExpression = new Regex( SCRIPT_PARSE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.ExplicitCapture); Match scriptMatch = scriptExpression.Match(html); // see if a match was found and extract the script if (scriptMatch.Success) { scriptTag = scriptMatch.Result(SCRIPT_TAG_PARSE_MATCH); scriptSource = scriptMatch.Result( SCRIPT_SOURCE_PARSE_MATCH); } } } // process link depending on if they are null or not if (linkTag != null) { // see if have a processable link tag bool linkFound = false; // extract the rel and type properties of the link tag Match relMatch = Regex.Match(linkTag, REL_PARSE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); Match typeMatch = Regex.Match(linkTag, TYPE_PARSE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); Match hrefMatch = Regex.Match(linkTag, HREF_PARSE_EXPRESSION, RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture); string relValue = (relMatch.Success) ? relMatch.Result(REL_VALUE_PARSE_MATCH) : string.Empty; string typeValue = (typeMatch.Success) ? typeMatch.Result(TYPE_VALUE_PARSE_MATCH) : string.Empty; string hrefValue = (hrefMatch.Success) ? hrefMatch.Result(HREF_VALUE_PARSE_MATCH) : string.Empty; if (IsStringEqual(relValue, REL_VALUE_STYLESHEET) && IsStringEqual(typeValue, TYPE_VALUE_TEXT_CSS) && hrefValue != string.Empty) { // populate the string value from the text string source = _wsrpService.GetResourceString(hrefValue); if (source != null) { // have a new style resource include document linkFound = true; if (styleSource == null) { // have no previous style so create styleSource = source; styleTag = STYLE_TAG_VALUE; } else { // append to previous style sheet styleSource = source + styleSource; } } } if (!linkFound) { // have link but not found so include in document mshtmlDomNode bodyNode = (mshtmlDomNode)body; mshtmlDomNode linkNode = (mshtmlDomNode)document.createElement(linkTag); linkNode = bodyNode.appendChild(linkNode); linkNode = linkNode.swapNode(bodyNode.firstChild); } } // process style depending on if they are null or not if (styleSource != null) { mshtmlDomNode bodyNode = (mshtmlDomNode)body; mshtmlDomNode styleNode = (mshtmlDomNode)document.createElement(styleTag); styleNode = bodyNode.appendChild(styleNode); styleNode = styleNode.swapNode(bodyNode.firstChild); mshtmlStyleElement style = (mshtmlStyleElement)styleNode; style.styleSheet.cssText = styleSource; } // process script depending on if they are null or not if (scriptSource != null) { mshtmlDomNode bodyNode = (mshtmlDomNode)body; mshtmlDomNode scriptNode = (mshtmlDomNode)document.createElement(scriptTag); scriptNode = bodyNode.appendChild(scriptNode); mshtmlScriptElement script = (mshtmlScriptElement)scriptNode; script.text = scriptSource; } }

After the initial markup is rendered, users can modify the WSRP mode, the window state, and the selected portlet. When a state change occurs, the viewer modifies the state within the service class and performs a WSRP getMarkup request.

The portlet connect dialog box lets the user switch portlets. If the connect dialog box is presented when a relationship has previously been established, it will display the previously acquired portlet information. If a new portlet is selected, the viewer control notifies the service class via the SelectPortlet method and performs a WSRP getMarkup request.

Processing Interactions

After markup has been rendered, the next step is to process interactions. Rendered markup contains action links structured as form post operations with an action attribute and anchor tags with a href attribute:

<FORM action=wsrp-urlType=blockingAction&amp;wsrp-interactionState=add method=post> <A href="https://wsrp-urlType=render&amp;wsrp-navigationalState=add"> Add Quote</A> <A href="https://wsrp-urlType=render&amp;wsrp-navigationalState=title"> Change Title</A>

Here, the referenced URLs are parsable WSRP expressions that appear in the form of parameters collections. The protocol, host name, port, and path have been excluded during URL rewriting because the viewer control is acting as the portal consumer.

During an interactive operation, the viewer control obtains the URL, processes the optional form data and collects file names to be uploaded. Once this data has been captured, the service class can assume control to make the WSRP message requests and return the new markup.

The service class ProcessInteraction method processes the WSRP interactions for the viewer control. The first step is to parse the requested URL, extracting the WSRP parameters. These parameter names, defined in the specification and configured using URL templates, determine the required navigational state, interaction state, WSRP mode, window state, and URL type. The URL type, in turn, determines the process that will take place. In this case, you're interested in render, resource, and blockingAction types.

The render type results in fetching new markup based on the new state, as defined by the other properties. The resource type provides a URL to which navigation is required. The blockingAction type results in the execution of a WSRP performBlockingInteraction request and the subsequent fetching of new markup. This is managed within the PerformBlockingInteraction method, which wraps the wsrpPerformBlockingInteraction method (see Figure 13). When executed, either an update response or redirection URL is returned.

Figure 13 PerformBlockingInteraction

// obtain the interaction response BlockingInteractionResponse interactionResponse = wsrpPerformBlockingInteraction(interactionState, formCollection, filenames); // persist any user specific information for later calls UpdateResponse updateResponse = interactionResponse.updateResponse; // see if one has an update response of redirectUrl if (updateResponse != null) { // persist any session context including a null value _sessionContext = updateResponse.sessionContext; // persist any new portlet information if (updateResponse.portletContext != null) { _portletPstates[_portletIndex].CloneHandle = updateResponse.portletContext.portletHandle; _portletPstates[_portletIndex].PortletState = updateResponse.portletContext.portletState; } // persist any new navigational and WSRP state _portletPstates[_portletIndex].NavigationalState = updateResponse.navigationalState; if (updateResponse.newMode != null && updateResponse.newMode != string.Empty) { _portletPstates[_portletIndex].WsrpMode = WsrpConversion.GetMode(updateResponse.newMode); } if (updateResponse.newWindowState != null && updateResponse.newWindowState != string.Empty) { _portletPstates[_portletIndex].WindowState = WsrpConversion.GetWindowState(updateResponse.newWindowState); } // obtain the markup and title response MarkupContext markupContext = updateResponse.markupContext; ExtractMarkup(markupContext, out html, out title); } // will return with unchanged information else RedirectUrl = interactionResponse.redirectURL;

The update response provides all the information relating to a state change in the portlet interactions. The portlet context, if returned, may contain a clone handle that requires persistence. This process is identical to the operation performed when a WSRP clonePortlet request is made.

Finally, any markup returned in the update response is processed. If the markup is not returned in this fashion, and no redirection URL is given, you must make a WSRP getMarkup request.

The parameters for a WSRP performBlockingInteraction request are the same as for a WSRP getMarkup request, but with the addition of the InteractionParams type. This contains the properties interactionState, portletStateChange, formParameters, and uploadContexts.

The main aspects of the interaction parameters are the form parameters and the upload context array. The form data is merely a collection of the WSRP NamedString types. The definition of the uploadContext property, consisting of a mimeType value and uploadData byte array, is a little more complex.

The specification for uploading binary data for WSRP is a bit unclear. The technique that has been adopted by most vendors is to pass the MIME contents, derived directly from a browser post operation, into the uploadData byte array with the MIME type defined as multipart/form-data. The MIME contents include all the attributes and form data, in addition to the binary data. The service class library provides a WsrpMimeBuilder helper class to support generating such a MIME stream. This helper class also encapsulates the urlmon.dll function FindMimeFromData to locate the content type of the data stream.

Once a blocking interaction has been performed, the markup or redirection URL is returned to the viewer control for processing.

As previously mentioned, the viewer control is responsible for obtaining the interaction URL, processing form data, and collecting file names to be uploaded. This is achieved by capturing and canceling the Web browser BeforeNavigate event and then executing the service class ProcessInteraction method. This returns new markup or a redirection URL.

The URL is easily obtained from the event arguments, but the post data and file names require more work. You can parse the post data, but since this may not contain a complete set of data when FileUpload elements are present, you can instead parse the HTML DOM. The parsing is outlined in Figure 14.

Figure 14 Parsing Markup

private void WsrpProcessInputs( out NameValueCollection eventCollection, out NameValueCollection formCollection, out NameValueCollection filenames) { eventCollection = new NameValueCollection(); formCollection = new NameValueCollection(); filenames = new NameValueCollection(); // see if active control is a submit button; ensure it's not cancel if (IsStringEqual(document.activeElement.tagName, HTML_INPUT_TAG)) { mshtmlInputElement activeElement = (mshtmlInputElement)document.activeElement; if (IsStringEqual(activeElement.type, FORM_SUBMIT_TYPE)) { formCollection.Add(activeElement.name, activeElement.value); eventCollection.Add(activeElement.name, activeElement.value); if (IsStringEqual(activeElement.value, FORM_CANCEL_TEXT)) return; } } // locate all the input elements on the form mshtmlElementCollection inputs = body.getElementsByTagName(HTML_INPUT_TAG); foreach (mshtmlElement element in inputs) { mshtmlInputElement inputElement = (mshtmlInputElement)element; string elementType = inputElement.type, filename = null; string elementName = inputElement.name, elementValue = null; bool eventArg = false; if (IsStringEqual(elementType, FORM_CHECKBOX_TYPE) || IsStringEqual(elementType, FORM_RADIO_TYPE)) { // see if input was a check box or radio button if (inputElement.@checked) elementValue = bool.TrueString; eventArg = true; } else if (IsStringEqual(elementType, FORM_TEXT_TYPE)) { // was text, excluding password and hidden values elementValue = inputElement.value; eventArg = true; } else if (IsStringEqual(elementType, FORM_PASSWORD_TYPE) || IsStringEqual(elementType, FORM_HIDDEN_TYPE)) { // was text, including password and hidden values elementValue = inputElement.value; } else if (IsStringEqual(elementType, FORM_FILE_TYPE)) { // was a file name filename = inputElement.value; eventArg = true; } if (elementValue != null) { // if have value, add to collection formCollection.Add(elementName, elementValue); if (eventArg) eventCollection.Add(elementName, elementValue); } if (filename != null) { // if have file, add it to the collection filenames.Add(elementName, filename); if (eventArg) eventCollection.Add(elementName, filename); } } }

The parsing process iterates through all the DOM elements, looking for the various input elements. The main complication is deciding which button was pressed during posting. This is done by looking at the active element and deciding if it was a cancel button. If so, the HTML DOM parsing is halted and only the cancel element is included within the form collection.

In addition to capturing the form elements for the WSRP interaction, the parsing process defines an additional form data collection that excludes hidden and password text. Whenever a user performs an interaction, the viewer control will raise an event containing this data collection along with the interaction URL. Other events are raised by the viewer control for user interactions such as portlet changes and WSRP mode changes.

State Persistence and Destruction of Relationships

WSRP producers can maintain state in the form of consumer registrations, portlets, and sessions. A consumer can create a registration context by calling the register service operation. In the scope of a consumer registration, portlets can be created by making a WSRP clonePortlet request. In regards to a portlet, the producer may create sessions that expire after a period of inactivity.

It's essential that a consumer application manage the destruction of state information. When a consumer no longer needs a portlet, it should execute a WSRP destroyPortlets request, passing the portlet handle, so the producer can release associated resources and session information. When a consumer no longer needs the producer's services, it should execute the WSRP deregister request, passing the consumer registration handle.

The service class needs to persist state information between sessions. It uses the PersistenceStateStorage helper class to do this, serializing and deserializing a PersistenceState type that aggregates a RegistrationPstate and an array of PortletPstate types. The PortletPstate type contains the portlet and clone handles, portlet state, navigational state, WSRP mode, and window state. During a normal interactive session, this information changes and all these changes need to be captured.

The storage mechanism is an isolated user storage file for the app domain. The file name is derived from the producer URL. Of course, there are times when isolated storage will not be the preferred mechanism, for example when an application needs to manage state centrally. To support this, the state information is exposed through the RegistrationPstate and PortletPstate properties and, thus, isolated storage persistence can be disabled.

An important aspect of the service class is the implementation of the IDisposable interface. The Dispose method ensures that any state information is persisted to isolated storage. The Dispose method of the viewer control correctly disposes of the service class so that, when the parent container is disposed, all state information is correctly persisted for the next session.

Since the state information is persisted in a file whose name is based upon the producer's URL, multiple service instances for a single producer will share the same persistence information. This, however, may not always be appropriate. Therefore, the service constructor supports a property used to differentiate service class instances. This value determines the isolated storage file name.

Once the producer state information is persisted, the consumer is responsible for ensuring that the resources are destroyed when no longer needed. For this, the service class exposes several methods, including ReleaseCookie, ReleaseSession, ReleasePortletClones, and ReleaseRegistration. The ReleaseCookie method merely initializes the cookie container associated with the portlet. ReleaseSession performs a WSRP releaseSession request, destroying the session information associated with the portlet interaction, while keeping any portlet clone and registration information intact.

The ReleasePortletClones method performs a WSRP destroyPortlets request for each clone created during interaction. In releasing the portlet clones, any interaction state, including edits made through the portlets, will be lost. This effectively initializes the state of the interaction. The heart of this process is shown in Figure 15.

Figure 15 Destroying Portlets

// create an array of the clones to be destroyed ArrayList handleList = new ArrayList(); for (int index = 0; index < _portletPstates.Length; index++) { if (!_portletPstates[index].IsCloneEmpty) { // add the clone to the list and clear out value handleList.Add(_portletPstates[index].CloneHandle); _portletPstates[index].CloneHandle = null; } } // if have items then call the destroy routine if (handleList.Count > 0) { string[] handles = new string[handleList.Count]; handleList.CopyTo(handles); DestroyPortletsResponse destroyPortletsResponse = wsrpDestroyPortlets(handles); }

To standardize the destruction of relationships when a service class is no longer needed, two termination methods are exposed: TerminateInteraction and TerminateRelationship. TerminateInteraction releases session information before executing the Dispose method. TerminateRelationship releases all persisted information, ending the consumer relationship with the producer, as shown here:

ReleasePortletClones(); ReleaseSession(); ReleaseRegistration(); _stateStorage.ClearPersistenceState(); _registrationPstate = new RegistrationPstate(); _portletPstates = null; Dispose();

The viewer control toolbar provides a disconnect action that lets the user control the destruction of WSRP persisted information. The user can choose to preserve or destroy the interaction information. If the data is to be preserved, the viewer control merely ends the WSRP interaction by calling the service class TerminateInteraction method. If the user disconnects from the WSRP producer, all persisted information is destroyed by calling TerminateRelationship.

Viewer Security

Security is important, especially when external data producers are involved. WSRP does not handle authentication and authorization; it relies on the underlying protocols and Web services standards. Thus, the WSRP service class relies on HTTP and WS-Security.

WSRP allows you to indicate the authentication to employ, but not how it is implemented. The options are wsrp:none, wsrp:password, or wsrp:certificate, and the value is passed in RuntimeContext. Consequently, the service class provides a means for specifying the authentication type and credentials. The authentication type is specified during the service initialization and can be set through AuthenticationMode to None, UseDomain, UseCertificate, or UsePassword. Domain forces the default login credentials to be used, and Password requires user credentials. To use these credentials, a class derived from ICredentials is defined to be used as the security credentials for the Web service proxy class:

ICredentials credentials; switch (_authenticationMode) { case WsrpAuthenticationMode.UsePassword: credentials = new NetworkCredential(_userName, _userPassword); break; case WsrpAuthenticationMode.UseDomain: credentials = CredentialCache.DefaultCredentials; break; default: credentials = null; break; }

To support the use of alternate credentials, a SetCredentials method is available. In this instance, the viewer control parent application has the responsibility of collecting and possibly saving these values in a secure manner.

The preferred alternative is to implement the WS-Security specification. This allows the use of a policy file for determining the security requirements, but it requires that the WSRP-compliant Web service supports this standard. If necessary, you can implement this feature by modifying the WSRP proxy class.

Note that certain resources, such as stylesheets, are obtained through a Web request so the security mechanism for these requests follows the same pattern as those for the Web service requests.

As the service description for any WSRP producer is standardized, you can generate the proxy class by pointing the .NET Framework WSDL tool to any sample WSDL.

Wrap-Up

Microsoft is a member of OASIS WSRP. Since the release of the 1.0 specification, Microsoft announced products that support the WSRP standard including two XML-based toolkits designed for SharePoint®. For the .NET implementation of a WSRP producer, I used NetUnity. The company provides access to a WSRP producer containing a number of sample portlets.

Carl Nolan is an independent technical architect and principle of Pavonis Consulting Services. He specializes in .NET Framework and SQL Server solutions. Previously, Carl was employed by Microsoft Consulting Services. Reach Carl at msdn@pavonis.co.uk.