2670

Web Client Software Factory Knowledge Base

Check out the knowledge base page at the Web Client Guidance Community site.

You Don’t Always Need Page Modules

From my article on Page Modules some people might have gotten the wrong impression that this would be the only way to get a reference to the page given its path, but it isn't.

You can use the System.Web.UI.PageParser.GetCompiledPageInstance method to get the reference to the page before calling the System.Web.HttpServerUtility.Transfer method.

There's one caveat, though. Unlike the version of the System.Web.HttpServerUtility.Transfer method where you that takes a virtual path as an argument, this one will not change the values of the System.Web.HttpRequest.AppRelativeCurrentExecutionFilePath and System.Web.HttpRequest.CurrentExecutionFilePath properties won't change from the original request.

That can either be a problem or the solution to a problem.

ASP.NET Resource Providers and HTTP Context

Sometime ago I built a custom resource provider for ASP.NET that relied on an attribute applied to the HTTP handler (usually a page) class instead of its virtual path to determine the location of the resources. Something like this:

[ResourceLocationAttribute(ResourceLocation)]
public partial class SomePage : Page
{
  // ...
}

Since the method signature for creating a local resource provider only has an argument with the virtual path of the HTTP handler I can ignore a way this argument and use the value of the current HTTP handler:

public class MyResourceProviderFactory : System.Web.Compilation.ResourceProviderFactory
{
  // ...
  
  public override System.Web.Compilation.IResourceProvider CreateLocalResourceProvider(string virtualPath)
  {
    string location = GetLocationFromCurrentHandlerAttribute(System.Web.HttpContext.Current.CurrentHandler);
    return CreateLocalResourceProviderFromLocation(location);
  }
  
  // ...
}

Until I tried to compile my site and get a NullReferenceException. Why? Because I don't have an HTTP context when I'm compiling my site with either Visual Studio or the ASP.NET Compilation Tool (Aspnet_compiler.exe).

Well, this is just a temporary drawback. I'll just create a resource provider without the location and initialize it on its first usage with an HTTP context, since that's what will happen at run time. But I do want to be sure that, when the method to create a local resource provider is called with an HTTP context (that will be at run time), the virtual path being passed is the current execution file path. And that's easy:

public class MyResourceProviderFactory : System.Web.Compilation.ResourceProviderFactory
{
  // ...
  
  public override System.Web.Compilation.IResourceProvider CreateLocalResourceProvider(string virtualPath)
  {
    if (!virtualPath.Equals(System.Web.HttpContext.Current.Request.CurrentExecutionFilePath, StringComparison.OrdinalIgnoreCase))
    {
    	throw new ArgumentException("Invalid path.", "virtualPath");
    }
    return CreateLocalResourceProvider();
  }
  
  // ...
}

Now I'm sure that every local resource provider created at run time is for the right resource.

Until I deployed it to an IIS application. At the first request ASP.NET compiles the whole site but something is different from when I was doing it in Visual Studio: there's an HTTP context with the current execution file path being for the requested page but the local resource provider is created for every page that has a resource expression in its markup and, surprisingly, not all of them correspond to the current execution file path.

I ended up just creating a local resource provider that saves the page's virtual path and is only initialized on the first request for a resource that has a current HTTP handler and checking, then, if the saved virtual path matches the current executing file path.

public class MyResourceProviderFactory : System.Web.Compilation.ResourceProviderFactory
{
  // ...
  
  public override System.Web.Compilation.IResourceProvider CreateLocalResourceProvider(string virtualPath)
  {
    return CreateLocalResourceProvider(virtualPath);
  }
  
  // ...
}

If you want to create your own resource provider for ASP.NET, here are a few articles that can help you:

The Cost Of Client-Side Redirects In ASP.NET And Using Page Modules

I've been asked if the Page Modules are really useful and if is there such an high cost on client-side redirects.

I hope this helps to shed some light to the issue.

Introducing ASP.NET Page Modules

(Republished at http://www.codeproject.com/useritems/PageHandlerFactoryWithMod.asp)  


Introduction


This article introduces the concept of Page Modules, which are similar to HTTP Modules but related to Page Life Cycle, and the need for them.


How HTTP Modules Work


According to the documentation, using an HTTP Module, "enables you to redirect the request to an alternative page, modify the request, or perform any other request manipulation". Well, at least up until you call Server.Transfer or Server.Execute, then you loose all the ability to tap into the Page Life Cycle of the served page.


How Page Modules Work


Page Modules enables you to handle every event in the Page Life Cycle of any page as well as its creation and release.


Each Page Module has an Init method that is called when the Page Handler Factory with Modules is initialized and each Page Module can subscribe to two events: PageCreated and PageReleasing.


In the PageCreated event arguments there are properties that expose the Page being created, the Context of the HTTP request, the HTTP data transfer method (GET or POST) that the client uses, the virtual path to the requested resource and the physical application path of the requested page. Using the reference to the page being created, you can subscribe to all events in the Page Life Cycle of the page being created.


In the PageReleasing event arguments there is a property that exposes the Page being released.


Page Modules versus HTTP Modules


You can implement much of the functionality of a Page Module as an HTTP Module if you are not calling Server.Transfer or Server.Execute. However, if you are using frameworks or application blocks like the Web Client Software Factory and its Page Flow Application Block that heavily rely on client redirects, you can greatly reduce network usage and server load by using a Page Module instead of an HTTP Module, at least, for part of its work.


Implementation of the Page Handler Factory With Modules


The implementation of the PageHandlerFactoryWithModules relies solely on sub-classing the PageHandlerFactory class and exposing events corresponding to the two methods of the IHttpHandlerFactory interface (GetHandler and ReleaseHandler).


public class PageHandlerFactoryWithModules : System.Web.UI.PageHandlerFactory
{
// ...

public sealed override IHttpHandler GetHandler(HttpContext context, string requestType, string virtualPath, string path)
{
Page page = base.GetHandler(context, requestType, virtualPath, path) as Page;

if (page != null)
{
OnPageCreated(new PageCreatedEventArgs(page, context, requestType, virtualPath, path));
}

return page;
}

public sealed override void ReleaseHandler(IHttpHandler handler)
{
Page page = handler as Page;

OnPageReleasing(new PageEventArgs(page));

base.ReleaseHandler(page);
}

// ...
}


As you can see, the PageHandlerFactoryWithModules class seals the GetHandler and ReleaseHandler methods and in replacement expose the OnPageCreated and OnPageReleasing that are responsible for triggering the PageCreated and PageReleasing events.


public class PageHandlerFactoryWithModules : System.Web.UI.PageHandlerFactory
{
// ...

protected virtual void OnPageCreated(PageCreatedEventArgs pageEventArgs)
{
TriggerEvent(pageCreatedEvent, pageEventArgs);
}

protected virtual void OnPageReleasing(PageEventArgs pageEventArgs)
{
TriggerEvent(pageReleasingEvent, pageEventArgs);
}

// ...
}


At the initialization of the PageHandlerFactoryWithModules class all modules are created from the configuration information and initialized.


public class PageHandlerFactoryWithModules : System.Web.UI.PageHandlerFactory
{
// ...

public PageHandlerFactoryWithModules()
{
// ...

InitModules();
}

private void InitModules()
{
PageModulesSection section = WebConfigurationManager.GetWebApplicationSection("PauloMorgado.web/pageModules") as PageModulesSection;

this.modules = section.CreateModules();

foreach (IPageModule module in this.modules)
{
module.Init(this);
}
}

// ...
}


As you can infer from the previous code block, a Page Module is a class that implements the IPageModule interface which has only one method called Init that receives a reference to the PageHandlerFactoryWithModules instance, just like with HTTP Modules.


public interface IPageModule
{
void Init(PageHandlerFactoryWithModules context);
}

(The full implementation of the Page Module concept is in the attached code download)


How to: Create Custom Page Modules


The custom page module described in this section subscribes to the PreInit event of the page being created to change its master page.


Creating the custom page module class


To implement a custom page module all you have to do is create a class that implements the IPageMoldule interface and handle de required events.


In the current example, the module needs to subscribe to the PreInit event of the page being created and in the event handler method changes the master page if the page has one.


public class MasterModule : IPageModule
{
public MasterModule()
{
}

public void Init(PageHandlerFactoryWithModules context)
{
context.PageCreated += PageCreatedHandler;
}

private static void PageCreatedHandler(object sender, PauloMorgado.Web.UI.PageEventArgs e)
{
e.Page.PreInit += PagePreInit;
}

private static void PagePreInit(object sender, EventArgs e)
{
Page page = (sender as Page);

if (page.MasterPageFile != null)
{
page.MasterPageFile = "~/Site.master";
}
}
}


Registering the custom page module


To register the page module, you'll need to add the PauloMorgado.web configuration section group and its inner pageModules configuration section.


Then, you'll have to remove the default declaration for the *.aspx path and add the PageHandlerFactoryWithModules class as the new *.aspx handler factory.


Finally, you can add your page modules in the configuration/PauloMorgado.web/pageModules configuration section.


<?xml version="1.0"?>
<
configuration>
<
configSections>
<
sectionGroup name="PauloMorgado.web">
<
section name="pageModules" type="PauloMorgado.Web.Configuration.PageModulesSection, PauloMorgado.Web.UI.PageHandlerFactoryWithModules"/>
</
sectionGroup>
</
configSections>
<
system.web>
<httpHandlers>
<
remove verb="*" path="*.aspx"/>
<
add verb="*" path="*.aspx" validate="false" type="PauloMorgado.Web.UI.PageHandlerFactoryWithModules, PauloMorgado.Web.UI.PageHandlerFactoryWithModules"/>
</
httpHandlers>
</
system.web>
<PauloMorgado.web>
<
pageModules>
<
add name="MasterModule" type="MasterModule"/>
</
pageModules>
</
PauloMorgado.web>
</
configuration>

Testing the custom page module


In the attached code download there's a sample of the master page changing module implemented as a page module and as an HTTP module.

Reference Implementation With Page Flow Navigation

In a previous post, I talked about Page Flow Navigation vs. Page Navigation.

This is an adaptation of the Reference Implementation to work with Page Flow Navigation. (You’ll need the changed version of the Page Flow Application Block.)

Downloads

PageFlowWithShoppingCartQuickStart Sample With Page Flow Navigation

In a previous post, I talked about Page Flow Navigation vs. Page Navigation.

This is an adaptation of the PageFlowWithShoppingCartQuickStart sample to work with Page Flow Navigation. (You’ll need the changed version of the Page Flow Application Block.)

Downloads

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

The Practices & Practices Team Is Requesting Feedback On What We Would Like Them To Build For The Web Client Software Factory

Glenn Blocks is requesting for feedback on what the p&p team should build for the next iteration of the Web Client Software Factory. If you have something in mind, let him know.

Refactor for ASP.NET version 2.2 is out

(This is not new, but ...)

Refactor! for ASP.NET version 2.2 is shipping. Built by Developer Express, this FREE code refactoring tool includes 29 time-saving refactorings and is available to all developers working in Visual Studio 2005 and Orcas Beta 1.

Read all about it in Mark's blog.