I've averaged 13.5 posts per month over the last 37 months to arrive at today's post #500.I'm not sure if I'll make another 500 posts, but I have pages of notes for posts covering ASP.NET, AJAX, Windows Workflow, WCSF, and more. I also have enough material for at least 15 more "What's Wrong with This Code" posts, which several people have told me are fun and educational. One day, I might even finish my "Design Patterns - A Love Story" post. Then again, maybe that story should be purged from the Internet forever.
Thanks for reading!
In the last post I pointed out problems you can experience trying to handle workflow events in an ASP.NET page. Before we get to a working solution, let's take a look at another pitfall.
We know the default WF scheduling service will select a thread from the CLR thread pool to execute workflows asynchronously. Generally speaking, this isn't a good approach for server side applications because we can tie up too many threads. Instead, we use the ManualWorkflowSchedulerService. The manual scheduler lets us run workflows on the same thread that is processing the HTTP request. We just need to call RunWorkflow on the service whenever we need a workflow to execute.
It's tempting to think the manual scheduler can make life easier since we don't have to worry about threads. For instance, let's suppose we want any unhandled faults originating from inside a workflow to bring the current request to a screeching halt. We want unhandled exceptions! When a workflow faults and throws an exception, WF will catch the exception and raise a WorkflowTerminated event. This event will fire on the same thread as the request that ran the faulty workflow.
Knowing that we want to end requests with an error, we could try to use global.asax like the following. After all, if we throw an exception on the current request thread, we should create the yellow screen of death (assuming the Page doesn't have an exception handler). Note: I'm using global.asax just to make this simple.
The problem is that the WorkflowRuntime is dedicated to its job. The runtime is tasked with telling all the event subscribers that a workflow terminated, and it is not going to let some unhandled exception prevent the rest of the subscribers from missing events. The WF runtime swallows our exception. See Ken Getz's article for how to use GetInvocationList to achieve this behavior.
How can we communicate the exception back to the Page? Consider the following code.
Now the web form can pull the exception out of the HTTP request context. Problem solved!
Oh, but wait - we are spiraling out of control. Now the ASP.NET developer has to remember to start workflows with the manual scheduler, and check the request context for exceptions, and use such and such a communication service to raise events to the workflow.
If you are going to the trouble to use Windows Workflow in an ASP.NET application, then you have an architectural responsibility to abstract away these problematic and sometimes dangerous scenarios to the point that the ASP.NET developer doesn't even know there is a workflow driving the process. Bring a mediator to the party.
Once you have the WorkflowRuntime up and running in an ASP.NET application or web service, you'll want handle key life cycle events like WorkflowTerminated and WorkflowCompleted. I want to warn you about some common pitfalls I've seen.
There are two subtle but extremely dangerous problems in the following code.
Think: Singletons and event handling. The WorkflowRuntime will typically be a singleton in an ASP.NET app. In the WF RTM version, you can have multiple WF runtimes executing inside the same AppDomain (this wasn’t true in the betas), but chances are you don't want to pay the performance overhead of spinning up a new WF runtime for each request. Thus, we have a single and globally accessible WF runtime. For questions about the ManualWorkflowSchedulerService, see my article on hosting Windows Workflow. This scheduler allows us to run the workflow instance synchronously - on the same thread as the request.
The WorkflowCompleted event will fire for any workflow that completes, not just the workflow we are executing inside this page (or web request).
Let's say the workflow is running some business rules to automatically approve or reject expense reports. This usually requires a human bean counter to get involved, but stick with me for a second and pretend it's all done in silicon. Let's say the workflow indicates approval or rejection with the OutputParameters property of the WorkflowCompletedEventArgs.
User Joe submits an expense report for his new red stapler. At the same time, I submit an expense report for a red Ferrari Enzo. There are two pages executing on the server, and both subscribed to the WorkflowCompleted event. If things work out just right, Joe's workflow will complete first and approve his expense report. The runtime will raise the completed event, which both pages handle. Since Joe's workflow indicated approval, Joe will soon be stapling papers while I get to go 0-60 in less than 3.5 seconds.
To fire the completed event, the WorkflowRuntime maintains a reference to the Page object. We should assume the WorkflowRuntime, as a singleton, will live for the duration of the AppDomain. Assuming the Page object never un-subscribes from the completed event, it will be in memory for the duration of the AppDomain, too. The garbage collector can't take the Page out of memory while it's being referenced by the WF runtime. RAM will start to disappear over time.
Even worse, with each request another instance of this Page class will wire itself to the WorkflowRuntime, and each time a workflow completes the WF runtime will need to raise the event to all these zombie page objects. This will keep the CPU busy, too.
It won't be long before the server is sucking mud, and the company is out of money.
One of the commercial applications I've worked on over the last few years has received consistent feedback from uses who dislike tree view controls. Many of these users are working with terminal emulators on a daily basis, so I first suspected that switching to a GUI came as a shock.
I did some research on tree controls and usability, but didn't uncover much information. Actually, many articles I found herald the tree view as the ideal solution for categorizing and drilling into large amounts of hierarchical information. Only Alan Cooper's book "About Face" turned up a caveat:
"Programmers tend to like this presentation [speaking of Tree controls]. It is often used as a file system navigator, and some find the format of the display to be effective – certainly more effective than scattering icons around in multiple windows on the desktop. Unfortunately, it is problematic for users because of the difficulty many nonprogrammer users with understanding hierarchical data structures. In general, it makes sense to use a treeview, no matter how tempting it may be, only in the case where what is being represented is "naturally" thought of as a hierarchy (such as a family tree). Using a treeview to represent arbitrary objects organized in an arbitrary fashion at the whim of a programmer is asking for big trouble when it comes to usability."
I've changed my initial suspicion. Now I suspect users don't have a problem with the mechanics of the tree view, but rather with the content of the tree view. It's not a case of how to work with the control, but where to start to get to the right node in the tree.
The more I think about the problem, the more I realize I face the same difficulty on an almost daily basis. When I call my cell phone company, I have to fit my specific problem into one of the three broad categories provided by the soothing sounds of an automated voice response system. If I initially choose the wrong category, I'm pretty well screwed and need to start over. This is frustrating.
A software alternative to the tree view is a search feature. Allow the user to get to the lowest level of detail without navigating a hierarchy of unfamiliar categories. This often works in the real world, too. If I can't find something in the categorized aisles of a hardware store, I ask someone who works there.
Interesting and somewhat related post: Luke Wroblewski has a look at the history of Amazon's Tab Navigation. I don't remember the last time I went to Amazon to browse categories, I always search for a specific item. It's well that they do away with tabs.
Searching capability is vital these days. I wonder, will System.SearchEngine will ever appear as a namespace in the .NET library?
Jason Diamond: "Not Delegates".
Pathfinder: JsUnit – Agile AJAX Development
Leave it to Microsoft to quantify everything.
The experience used to be emotional. There was the gentle purr of a power supply fan with the scent of plastics in the air. The caress of a firm, rubberized nipple would make pixels dance in front of my eyes. Those days are over.
My Windows Experience is now a cold, calculated number.
What's that you say? A low score is bad? Oh.
Well, this desktop was a top of the line computer…
… back in the year 2000.
If you want to use Windows Workflow in ASP.NET, you'll need create and maintain a WorkflowRuntime instance. What's the easiest approach to use?
The easiest approach (not necessarily the best), is to use global.asax.
This approach gives us a WorkflowRuntime we can reach from any page. Remember, in an ASP.NET Web Site project, the runtime code-generates a strongly-typed ApplicationInstance property from global.asax for each web form. We can access the WorkflowRuntime property defined in global.asax with ease:
This approach is simple, straightforward, and easy, but let's think about the disadvantages.
First, global.asax makes the WF runtime readily available to the web application, but it's not so easy to obtain this reference from a data, business, or service layer. It's possible - but not pretty.
Secondly, global.asax can cause headaches. I tend to avoid adding global.asax to a project, as once the file comes into play it tends to become a dumping ground for global "stuff" that an application needs.
Generally speaking, a better approach is to manage the WF runtime from a lower layer in the application architecture. Using a Registry or Service Locator type pattern makes the runtime available through all layers of the application. Error reporting, logging, and tracking of workflows and their runtime can live inside one or more dedicated components. This approach requires a little more work, but provides the flexibility required for larger applications. More details to come.