Page Flow Navigation vs. Page Navigation

Introduction

I’ve been working with page flows for the past days and the idea of navigating to a page flow instead of navigating between pages belonging to a pageflow is making more sense each time I think about it.

The Web Client Software Factory comes with a Page Flow Application Block that governs the navigation between pages of the same page flow.

The way the Page Flow Application Block identifies a particular page flow is by the URLs of the pages that belong to that page flow. This is possible because you are still navigating to those pages. This also makes it impossible to reuse the same page between page flows.

In this post I will change the Page Flow Application Block (keeping the changes to a minimum) to provide page flow navigation instead of page navigation.

Changing the Page Flow Definition

In order to keep changes to a minimum, we’ll use the Name property of the page flow as its URL. This will be the virtual address of the page flow and will also be used as the AbortPage’s URL if one is not provided.

Since the individual pages will no longer be directly accessible, we can drop all methods that correlate pages with URLs:

  • IPageFlowDefinition.ContainsUrl(string url);
  • IPageFlowDefinition.GetPageFromUrl(string url);

Changing the Page Flow Definition Catalog

Since we are navigating to page flows, we don’t need to keep track of the page’s URLs, which makes the definition catalog simpler:

/// <summary>
/// A catalog of <see cref="IPageFlowDefinition"/>s.
/// </summary>
public class PageFlowDefinitionCatalog : IPageFlowDefinitionCatalog
{
    private Dictionary<string, IPageFlowDefinition> catalog = new Dictionary<string, IPageFlowDefinition>();

    /// <summary>
    /// Gets the number of <see cref="IPageFlowDefinition"/>s in the catalog.
    /// </summary>
    /// <value>The number of <see cref="IPageFlowDefinition"/>s in the catalog.</value>
    public int Count
    {
        get { return this.catalog.Count; }
    }

    /// <summary>
    /// Removes an <see cref="IPageFlowDefinition"/> from the catalog.
    /// </summary>
    /// <param name="definition">The definition to remove.</param>
    public void Remove(IPageFlowDefinition definition)
    {
        this.catalog.Remove(definition.Name);
    }

    /// <summary>
    /// Adds an <see cref="IPageFlowDefinition"/> to the catalog.
    /// </summary>
    /// <param name="definition">The <see cref="IPageFlowDefinition"/> to add to the catalog.</param>
    public void Add(IPageFlowDefinition definition)
    {
        this.catalog.Add(definition.Name, definition);
    }

    /// <summary>
    /// Get an <see cref="IPageFlowDefinition"/> with a specific URL from the catalog.
    /// </summary>
    /// <param name="rawUrl">The URL to search for.</param>
    /// <returns>The <see cref="IPageFlowDefinition"/> with a matching URL.</returns>
    public IPageFlowDefinition GetByUrl(string rawUrl)
    {
        IPageFlowDefinition definition;
        this.catalog.TryGetValue(rawUrl, out definition);
        return definition;
    }
}

Changing the WorkflowFoundationPageFlowDefinition

In order to comply to these changes, the provided implementation (WorkflowFoundationPageFlowDefinition) will need to be changed.


If an abort page URL is not provided it will default to the not running URL.


/// <summary>
/// Implementation of a page flow definition that relies on the <see cref="Activities.PageFlow">PageFlow activity</see>.
/// </summary>
/// <remarks>This class is used to query the page flow metadata and is accessible through the <see cref="WorkflowFoundationPageFlow.Definition"></see> propert
public class WorkflowFoundationPageFlowDefinition : IPageFlowDefinition
{
    // ...

    /// <summary>
    /// Initializes a new instance of the <see cref="WorkflowFoundationPageFlowDefinition"></see> given a <see cref="Activities.PageFlow">PageFlow activity</s
    /// </summary>
    /// <param name="definition">A <see cref="Activities.PageFlow">PageFlow</see></param>
    /// <exception cref="ArgumentNullException"><paramref name="key"/> is <see langword="null"></see></exception>
    public WorkflowFoundationPageFlowDefinition(Activities.PageFlow definition)
    {
        if (definition == null)
            throw new ArgumentNullException("definition");

        _definition = definition;
        _abortPage = new Page("PageFlow.AbortPage", (string.IsNullOrEmpty(definition.NotRunningUrl) ? this._definition.Name : definition.AbortUrl));
    }

    // ...
}

Changing the Page Flow Provider


The implementation of IPageFlowProvider.ProcessRequest(string url) will need to change, although keeping the signature.


The ProcessResult class will need to be changed. Besides redirecting or doing nothing, there will be a new action: Rewrite.


/// <summary>
/// An object describing the results of a ProcessRequest() call on 
/// <see cref="PageFlowHttpModule"/>.
/// </summary>
public class ProcessResult
{
    /// <summary>
    /// Possible actions to take as a result of the URL processing.
    /// </summary>
    public enum ProcessAction : int
    {
        /// <summary>
        /// No action to be taken.
        /// </summary>
        None = 0,

        /// <summary>
        /// Rerwrite the URL to execute the page.
        /// </summary>
        Rewrite = 1,

        /// <summary>
        /// Redirect the request.
        /// </summary>
        Redirect = 2
    }

    private string _url;
    private ProcessAction _action;

    /// <summary>
    /// Creates an instance of a ProcessResult.
    /// </summary>
    public ProcessResult()
    {
    }

    /// <summary>
    /// Creates an instance with the specified properties.
    /// </summary>
    /// <param name="action">The action to process.</param>
    /// <param name="url">The URL.</param>
    public ProcessResult(ProcessAction action, string url)
    {
        _action = action;
        _url = url;
    }

    /// <summary>
    /// Gets the URL to redirect to.
    /// </summary>
    public string Url
    {
        get { return _url; }
    }

    /// <summary>
    /// Gets the determiniation if the request should be redirected to the Url.
    /// </summary>
    public ProcessAction Action
    {
        get { return _action; }
    }
}

Rewrite will change the HTTP request in order to execute the page registered for the current state.


Changing the WorkflowFoundationPageFlowProvider

In order to comply to these changes, the provided implementation (WorkflowFoundationPageFlowProvider) will need to be changed.


If the navigation is to a page fllow, the provider will need to instruct the HTTP module to rewrite the path. So, every method creating an instance of ProcessResult wil need to be changed.


/// <summary>
/// Implementation that uses Windows Workflow Foundation as the page flow engine.
/// </summary>
/// <remarks>
/// <para>This class is provided as a singleton by the <see cref="PageFlowDirectory"/> class</para>
/// </remarks>
public class WorkflowFoundationPageFlowProvider : IPageFlowProvider, IDisposable
{
    // ...

    /// <summary>
    /// Process the request when there is no instance in the store or the instance is NOT running.
    /// </summary>
    /// <param name="url">The request url.</param>
    /// <returns>The <see cref="ProcessResult" /> holding the redirect action to take.</returns>        
    private ProcessResult ProcessWithNoRunningInstance(string url)
    {
        ProcessResult result;
        IPageFlowDefinition definition = PageFlowDirectory.Catalog.GetByUrl(url);
        if (definition != null)
        {
            IPageFlow instance = GetPageFlow(definition.PageFlowType, false);
            if (instance == null)
            {
                result = ProcessWithNonExistingInstance(definition);
            }
            else
            {
                result = ProcessWithExistingInstance(definition, instance);
            }
        }
        else
        {
            // Do nothing.
            result = new ProcessResult(ProcessResult.ProcessAction.None, string.Empty); ;
        }

        return result;
    }

    /// <summary>
    /// Process the request with a running page flow instance.
    /// </summary>
    /// <param name="instance">The running page flow instance.</param>
    /// <param name="url">The request url.</param>
    /// <returns>The <see cref="ProcessResult" /> holding the redirect action to take.</returns>        
    private ProcessResult ProcessWithRunningInstance(IPageFlow instance, string url)
    {
        // The default action is to Rewrite to execute the current state's page.
        ProcessResult result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, instance.CurrentPage.Url);

        if (!(instance.Definition.Name.Equals(url, StringComparison.OrdinalIgnoreCase)))
        {
            switch (instance.Definition.Abandonable)
            {
                case AbandonBehavior.AllowAndDiscardInstance:
                    instance.Abort(false);
                    result = ProcessRequest(url);
                    break;
                case AbandonBehavior.AllowAndSaveInstance:
                    instance.Suspend();
                    result = ProcessRequest(url);
                    break;
                case AbandonBehavior.Prevent:
                    // Redirect to the current page flow.
                    result = new ProcessResult(ProcessResult.ProcessAction.Redirect, instance.Definition.Name);
                    break;
            }
        }
        return result;
    }

    /// <summary>
    /// Process the request using the specified instance.
    /// </summary>
    /// <param name="definition">The page flow definition corresponding to the request url.</param>
    /// <param name="instance">The existing page flow instance.</param>
    /// <returns>The <see cref="ProcessResult" /> holding the redirect action to take.</returns>        
    private ProcessResult ProcessWithExistingInstance(IPageFlowDefinition definition, IPageFlow instance)
    {
        ProcessResult result;
        switch (definition.Abandonable)
        {
            case AbandonBehavior.AllowAndSaveInstance:
                result = ResumeInstance(instance, definition);
                break;
            default:
                // Redirect to the current page flow.
                result = new ProcessResult(ProcessResult.ProcessAction.Redirect, HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath);
                break;
        }
        return result;
    }

    private ProcessResult ProcessWithNonExistingInstance(IPageFlowDefinition definition)
    {
        ProcessResult result;
        if (HttpContext.Current != null && HttpContext.Current.Request.HttpMethod.Equals("POST", StringComparison.OrdinalIgnoreCase))
        {
            // NOTE: this special case happens when there is no running instance,
            //       there are no suspended instances, and the url belongs to a page flow
            //       but we are on PostBack. In this case we let the request flow, if not we
            //       send the user to NotRunning url.
            // Redirect to the current page flow.
            result = new ProcessResult(ProcessResult.ProcessAction.Redirect, definition.Name);
        }
        else
        {
            // Rewrite to execute the current page flow's not running page.
            result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, definition.NotRunningRedirect);
        }
        return result;
    }

    private ProcessResult ResumeInstance(IPageFlow instance, IPageFlowDefinition definition)
    {
        ProcessResult result;
        if (instance.Status == PageFlowStatus.Suspended)
        {
            // Rewrite to execute the current page flow's not running page.
            result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, definition.NotRunningRedirect);
        }
        else
        {
            if (instance.CurrentPage != null)
            {
                // Rewrite to execute the current state's page.
                result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, instance.CurrentPage.Url);
            }
            else
            {
                // Rewrite to execute the current page flow's not running page.
                result = new ProcessResult(ProcessResult.ProcessAction.Rewrite, definition.NotRunningRedirect);
            }
        }
        return result;
    }
}

Changing the Page Flow HTTP Module


With the Page Flow Application Block, every thing starts in its HTTP module. So, we also need to make a few changes here.


Instead of processing the request in the PostAcquireRequestState event, the request will be processed in two steps:


  1. In the PostAuthorizeRequest event, the page flow provider will be requested to process the request. Because the path to the executing pages is being rewritten, this needs to be done even if no session state is loaded.
  2. In the PostMapRequestHandler event, if the path was rewritten in the PostAcquireRequestState event, it’s rewritten back to the original URL.

/// <summary>
/// An <see cref="IHttpModule"/> for working with PageFlows
/// </summary>
public class PageFlowHttpModule : IHttpModule
{
    private ProcessResult result;
    private string originalUrl;

    // ...

    /// <summary>
    /// Initializes the PageFlowHttpModule to work with the <see cref="HttpApplication"/>.
    /// </summary>
    /// <param name="context">The <see cref="HttpApplication"/>.</param>
    public void Init(HttpApplication context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        context.PostAuthorizeRequest += new EventHandler(OnPostAuthorizeRequest);
        context.PostMapRequestHandler += new EventHandler(OnPostMapRequestHandler);

        WebConfigurationManager.OpenWebConfiguration(null);
        HttpRequestHelper.LoadExtensionsHandledByPageHandlerFactoryFromConfig((HttpHandlersSection)WebConfigurationManager.GetSection("system.web/httpHandlers"));
    }

    /// <summary>
    /// Processes an <see cref="HttpRequest"/>.
    /// </summary>
    /// <param name="request">The request to process</param>
    /// <returns>A <see cref="ProcessResult"/>.</returns>
    public ProcessResult ProcessRequest(HttpRequest request)
    {
        Guard.ArgumentNotNull(request, "request");

        ProcessResult result = new ProcessResult();

        if (HttpRequestHelper.IsHandledByPageHandlerFactory(request.AppRelativeCurrentExecutionFilePath))
        {
            result = PageFlowDirectory.Provider.ProcessRequest(request.AppRelativeCurrentExecutionFilePath);
        }
        return result;
    }

    private void OnPostAuthorizeRequest(object sender, EventArgs e)
    {
        this.result = ProcessRequest(HttpContext.Current.Request);
        switch (result.Action)
        {
            case ProcessResult.ProcessAction.Redirect:
                HttpContext.Current.Response.Redirect(this.result.Url, true);
                break;
            case ProcessResult.ProcessAction.Rewrite:
                System.Diagnostics.Debug.WriteLine(this.result.Url);
                this.originalUrl = HttpContext.Current.Request.AppRelativeCurrentExecutionFilePath;
                HttpContext.Current.RewritePath(this.result.Url, true);
                break;
        }
    }

    void OnPostMapRequestHandler(object sender, EventArgs e)
    {
        if ((this.result != null) && (this.result.Action == ProcessResult.ProcessAction.Rewrite))
        {
            System.Diagnostics.Debug.WriteLine(this.result.Url);
            HttpContext.Current.RewritePath(this.originalUrl, true);
        }
        this.result = null;
    }
}

Conclusion


This implementation has several benefits:


  • Deployment – There is no need to deploy the URL of each page. The entire set of pages of a page flow can be changed without having to notify users or reconfigure menus.
  • Security – This adds two kinds of security:
    • Security by obscurity – The user will never now how many pages make a particular page flow.
    • URL security – The “real” pages can, and should, be blocked from acess to the users.
    • Page flow security – The user can never jump into a particular state.

Disclaimer


This implementation has not been fully tested and has known problems.


Downloads

Are You Forbidden From Accessing Your Latest MSDN Shipments?

It’s probably because you have the “wrong” main browser language.

I’ve been trying, for over a year, to view my Latest MSDN Shipments with no success. Apparently, I’m forbidden (HTTP response status code 403) to access.

I’ve contacted MSDN subscription’s support several times which has been of no help at all. They seem to be convinced that if they can access it and I delete all my cookies and offline content it will all work; if not, then they are clueless (even with the IEWatch logs I’ve sent).

All this time was able to access in a work colleague’s system and today a thought just popped my mind: “What can possible be the difference between our two systems?”

I jumped right to the first obvious option: browser languages. And, BINGO! My main language is Portuguese (Portugal) [pt-PT] and my colleague’s is English (United States) [en-US].

As soon as I set my main language to English (United States) [en-US], I was no longer forbidden to view my Latest MSDN Shipments.

Mind you that I do have a secondary setting of English (United States) [en-US] on my browser languages.

I’ve tried a few more languages and here are the results:

Language Status
English (United States) [en-US] Allowed
French (France) [fr-FR] Allowed
German (Germany) [de-DE] Allowed
Spanish (International Sort) [es-ES] Allowed
Italian (Italy) [it-IT] Forbidden
Dutch (Netherlands) [nl-NL] Forbidden
Portuguese (Portugal) [pt-PT] Forbidden

Windows Live Does Care About Users

At least, Windows Live Search does.

After my post about the way non English speakers are treated by Windows Live, I’ve been contacted by a Program Manager from Live Search and an Int’l Lead Program Manager.

As it turned out, I had my Live Search settings to display the pages in “Portuguese (Brasil)”. This means that everything is working fine with Live Search – correct display setting (pt-BR) for the correct market (pt-PT).

They might want to check the spelling for the Brazilian version though. In Portuguese, country names can have gender or not. It might look odd to not use a gender when it should but it’s definitely wrong to use one when it shouldn’t be used or to use the wrong one.

The problem with the Brazilian version is that it’s always using the contraction of the preposition de (from in English) with the definite article o (masculine form of the English the) which makes do (da is the feminine version). This looks OK with Brazil (masculine) but looks odd with Portugal (no gender) or France (feminine).

I’m no language expert but I don’t think there’s a rule for when using a gender and which one should be used (it might even differ between Portugal and Brazil). I guess they would need a definition table for that.

The Portuguese version uses only the preposition de without the contraction with a definite article. And in this case it doesn’t look odd at all.

Now I feel like a first class citizen, at least in Windows Live Search.

I still think that Google‘s approach is simplier and better. I just don’t use it because I sold my soul to Microsoft (as I’ve been told many times).

Why Does Windows Live Insist In Treating Non English Speaking Users As 2nd (or 3rd) Class Users

UPDATED

I’m Portuguese. So, I have my web browser language settings set up as “pt-PT; en-US”. I always thought that meant that I want Portuguese (Portugal) content if available; otherwise I want English (U.S.) content.

With these settings, when I browse to http://msdn.microsoft.com I get the general U.S. English content and a nice section of Portuguese (Portugal) content (Announcements).

In some other places of the enormous microsoft.com I usually get Portuguese (Brasil) when no Portuguese (Portugal) content is available (like MSDN Magazine articles). I can live with that because I can always change some setting (even if it’s in the URL) to get the content in English (U.S.).

But with Windows Live it’s way different.

Let’s start with Search. If I want to use Windows Live Search (http://search.live.com/) I get redirected to http://www.live.com/?searchonly=true&mkt=pt-BR which is very useful if I’m looking, say, for a washing machine (máquina de lavar). I get all those nice links for shops where to buy a washing machine but I can’t get there because Windows Local Live can’t get me driving directions to get across the Atlantic Ocean (Google Maps gives me driving directions with the caveat that I have to get wet).

If I do not want to get across the Atlantic Ocean, I still have a check box to choose “Only from the Portugal” (exact translation) or “Only in Portuguese (Brasil)”. But if I want English (U.S.) I need to know the URL switch “mkt=en-US”. Google, on the other hand, acknowledges the fact that I’m Portuguese (from Portugal) and always redirects me to Google Portugal and doesn’t confuse me with a Brazilian user. Even if I go to Google Brasil I can choose to see it in my Portuguese or there’s. In either cases, there’s a distinction between language and location. I can search Portuguese content in either cases but I can choose content in Portugal or Brazil depending on the site. Also, in both sites, I have a link to go to the main international site.

I saw this nice search box in a blog and I thought it would look nice in my blog. I followed the Get my own Search Box! link and found out that “the page I was looking for was not found”. Why? Because my main browser language is not “en-US”, that’s why.

Windows Live Writer beta 2 is out and I tried to get it but couldn’t (“mkt=en-US” doesn’t work there). The same thing with the Windows Live Messenger 8.5 beta. Fortunately, Scott give direct links before I had found out I needed to change my browser’s main language.

Now I’m happily blogging with Windows Live Writer beta 2, but although I run an en-US version of Windows, I have my regional settings set to pt-PT and, to get spell checking for English I have to use this trick.

Sometimes it’s not just about how good your are, it’s also about how good you treat your users.

Sharing My Feed Readings

I’ve been using NewsGator and it’s FeedDemon for a while.

One of the features of FeedDemon are the News Bins:

Store news items in a central location and provide a handy way to collect items from different channels. If you find an interesting item that you might want to read again, just store it in a news bin for future reference. News Bins are synchronized through the NewsGator Online platform, so you can read these items from FeedDemon on other computers as well as other NewsGator readers.

News Bins can be shared as an RSS Feed and I’m sharing one via FeedBurner. If you are curios about what I find interesting, just subscribe to it.

Subscribe to my readings' feed

Microsoft Certified Professional Transcript

Microsoft Certification Status


Certification Version Date Achieved
Microsoft Certified Technology Specialist May 31, 2007
.Net Framework 2.0: Web Applications May 31, 2007
.Net Framework 2.0: Windows Applications May 31, 2007
Microsoft Certified Solution Developer Dec 22, 2004
For Microsoft .NET Dec 22, 2004
Microsoft Certified Application Developer May 28, 2003
For Microsoft .NET May 28, 2003
Microsoft Certified Professional Apr 09, 2003

Microsoft Certification Exams Completed Successfully


Exam ID Description Date Completed
553 UPGRADE: MCSD Microsoft® .NET Skills to MCPD Enterprise Application Developer by Using the Microsoft® .NET Framework: Part 1 May 31, 2007
340 Implementing Security for Applications with Microsoft Visual C#® .NET Jun 29, 2005
300 Analyzing Requirements and Defining Microsoft .NET Solution Architectures Dec 22, 2004
316 Developing and Implementing Windows®-based Applications with Microsoft® Visual C#™ .NET and Microsoft® Visual Studio® .NET Dec 22, 2004
229 Designing and Implementing Databases with Microsoft® SQL Server™ 2000 Enterprise Edition May 28, 2003
310 Developing XML Web Services and Server Components with Microsoft® Visual Basic® .NET and the Microsoft® .NET Framework May 05, 2003
305 Developing and Implementing Web Applications with Microsoft® Visual Basic® .NET and Microsoft® Visual Studio® .NET Apr 09, 2003

Microsoft Certification Transcript Sharing Code


Transcript ID 661219
Access Code PJMorgado
http://www.microsoft.com/learning/mcp/transcripts