MoveWidgetInstanceWorkflow verifies whether the widget being moved is really the current user’s widget. This is necessary to prevent malicious web service hacking (see the “Implementing Authentication and Authorization” section in Chapter 3). TheEnsureOwnerActivitycan check both the page and the widget’s ownership (see Example 4-24).
Example 4-24. EnsureOwnerActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { var db = DatabaseHelper.GetDashboardData();
if( this.PageId == 0 && this.WidgetInstanceId == 0 ) { throw new ApplicationException("No valid object specified to check"); }
if( this.WidgetInstanceId > 0 ) { // Gets the user who is the owner of the widget. Then sees if the current user is the same. var ownerName = (from wi in db.WidgetInstances where wi.Id == this.WidgetInstanceId select wi.Page.AspnetUser.LoweredUserName).First();
if( !this.UserName.ToLower().Equals( ownerName ) ) throw new ApplicationException(string.Format("User {0} is not the owner of the widget instance {1}", this.UserName, this.WidgetInstanceId)); }
if( this.PageId > 0 ) { // Gets the user who is the owner of the page. Then sees if the current user is the same. var ownerName = (from p in db.Pages where p.ID == this.PageId select p.AspnetUser.LoweredUserName).First();
if( !this.UserName.ToLower().Equals( ownerName ) ) throw new ApplicationException(string.Format("User {0} is not the owner of the page {1}", this.UserName, this.PageId)); }
return ActivityExecutionStatus.Closed; }
EnsureOwnerActivitytakesUserNameand eitherWidgetInstanceIdorPageId and verifies the user’s ownership. It should climb through the hierarchy fromWidgetInstanceto thePageand then toAspnetUser to check whether the username matches or not. If the username is different than the one specified, then the owner is different and it’s a malicious attempt.
CheckingPageownership requires just going one level up toAspnetUser. But checkingWidgetInstanceownership requires going up to the container page and then checking ownership of the page. This needs to happen very fast because it is called on almost every operation performed onPage orWidgetInstance. This is why you want to make sure it does a scalar select only, which is faster than selecting a full row.
Once the owner has been verified, the widget can be placed on the right column. The next activity,PutWidgetInstanceInWorkflow, does nothing but put theWidgetInstanceobject into a public property according to its ID so the object can be manipulated directly. The other activities in the workflow work with the object’sColumnNoandOrderNoproperties. The next step,PushWidgetsDownInNewColumn, calls thePushDownWidgetsOnColumnActivity, which pushes widgets down one row so there’s a room for a new widget to be dropped (see Example 4-25).
Example 4-25. PushDownWidgetsOnColumnActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { var db = DatabaseHelper.GetDashboardData(); var query = from wi in db.WidgetInstances where wi.PageId == PageId && wi.ColumnNo == ColumnNo && wi.OrderNo >= Position orderby wi.OrderNo select wi; List<WidgetInstance>list = query.ToList();
int orderNo = Position+1; foreach( WidgetInstance wi in list ) { wi.OrderNo = orderNo ++; }
db.SubmitChanges();
return ActivityExecutionStatus.Closed; }
The idea is to move all the widgets right below the position of the widget being dropped and push them down one position. Now we have to update the position of the dropped widget using the activityChangeWidgetInstancePositionActivity(see Example 4-26).
Example 4-26. ChangeWidgetInstancePositionActivity Execute function
The widget is placed on a new column, and the old column has a vacant place. But now we need to pull the widgets one row upward on the old column.ReorderWidgetInstanceOnColumnActivityfixes row orders on a column, eliminating the gaps between them (see Example 4-27). The gap in the column will be fixed by recalculating the row number for each widget on that column, starting from zero.
Example 4-27. ReorderWidgetInstanceOnColumnActivity Execute function
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext) { var db = DatabaseHelper.GetDashboardData(); var query = from wi in db.WidgetInstances where wi.PageId == PageId && wi.ColumnNo == ColumnNo orderby wi.OrderNo select wi; List<WidgetInstance>list = query.ToList();
int orderNo = 0; foreach( WidgetInstance wi in list ) { wi.OrderNo = orderNo ++; }
db.SubmitChanges();
return ActivityExecutionStatus.Closed; }
That’s all that is required for a simple drag-and-drop operation.
DashboardFacade provides a single entry point to the entire business layer. It provides easy-to-call methods that run workflows. For example, the NewUserVisit function executes the NewUserSetupWorkflow (see Example 4-28).
Example 4-28. DashboardFacade.NewUserVisit calls NewUserSetupWorkflow and creates a complete setup for a new user on the first visit
public UserPageSetup NewUserVisit() { using( new TimedLog(this._UserName, "New user visit") ) { var properties = new Dictionary<string,object>(); properties.Add("UserName", this._UserName); var userSetup = new UserPageSetup(); properties.Add("UserPageSetup", userSetup);
Here the input parameter to the workflow isUserName. Although theUserPageSetupobject is passed as if it was an input parameter, it’s not an input. You are passing a null object, which the workflow will populate with loaded data. It’s like an out parameter in function calls. The workflow will populate this parameter’s value once it completes the execution.
Other methods, likeLoadUserSetup,DeleteWidgetInstance, andMoveWidgetInstance, behave the same way. They take necessary parameters as input and pass them to their own workflows, e.g., theMoveWidgetInstancefunction (see Example 4-29).
Example 4-29. DashboardFacade.MoveWidgetInstance calls MoveWidgetInstanceWorkflow to move a widget from one position to another
public void MoveWidgetInstance( int widgetInstanceId, int toColumn, int toRow ) { using( new TimedLog(this._UserName, "Move Widget:" + widgetInstanceId) ) { var properties = new Dictionary<string,object>(); properties.Add("UserName", this._UserName); properties.Add("WidgetInstanceId", widgetInstanceId); properties.Add("ColumnNo", toColumn); properties.Add("RowNo", toRow);
However, getting a return object from a workflow is quite complicated. TheAddWidgetfunction in the façade needs to get the newly added widget instance out of the workflow (see Example 4-30).
Example 4-30. DashboardFacade.AddWidget function calls AddWidgetWorkflow to add a new widget for the user’s current page
public WidgetInstance AddWidget(int widgetId) { using( new TimedLog(this._UserName, "Add Widget" + widgetId) ) { var properties = new Dictionary<string,object>(); properties.Add("UserName", this._UserName); properties.Add("WidgetId", widgetId);
// New Widget instance will be returned after the workflow completes properties.Add("NewWidget", null);
return properties["NewWidget"] as WidgetInstance; } }
A null object is being passed here to theNewWidget property of the workflow:AddWidgetWorkflow, which will populate this property with a new instance ofWidgetwhen it completes. Once the workflow completes, the object can be taken from the dictionary.
WorkflowHelper is a handy class that makes implementing a workflow a breeze, especially when used with ASP.NET. In the business layer, the workflow needs to be synchronously executed, but the default implementation of WF is to work asynchronously. Also, you need return values from workflows after their execution is complete, which is not so easily supported due to the asynchronous nature of the workflow. Both of these tasks require some tweaking with the workflow runtime to successfully run in the ASP.NET environment.
TheWorkflowHelper.Initfunction initializes workflow runtime for the ASP.NET environment. It makes sure there’s only one workflow runtime per application domain. Workflow runtime cannot be created twice in the same application domain, so it stores the reference of the workflow runtime in the application context. Example 4-31 shows its partial code.
Example 4-31. WorkflowHelper.Init, part 1
public static WorkflowRuntime Init() { WorkflowRuntime workflowRuntime;
// Running in console/winforms mode, create an return new runtime and return if( HttpContext.Current == null ) workflowRuntime = new WorkflowRuntime(); else { // running in web mode, runtime is initialized only once per // application if( HttpContext.Current.Application["WorkflowRuntime"] == null ) workflowRuntime = new WorkflowRuntime(); else return HttpContext.Current.Application["WorkflowRuntime"] as WorkflowRuntime; }
The initialization takes care of both ASP.NET and the Console/Winforms mode. You will need the Console/Winforms mode when you test the workflows from a console application or from unit tests. After the initialization, it registersManualWorkflowSchedulerService, which takes care of synchronous execution of the workflow.CallWorkflowactivity, which is explained inNewUserSetupWorkflow, uses theActivities.CallWorkflowServiceto run another workflow synchronously within a workflow. These two services make WF usable from the ASP.NET environment (see Example 4-32 ).
Example 4-32. WorkflowHelper.Init, part 2
var manualService = new ManualWorkflowSchedulerService(); workflowRuntime.AddService(manualService);
var syncCallService = new Activities.CallWorkflowService(); workflowRuntime.AddService(syncCallService);
workflowRuntime.StartRuntime();
// on web mode, store the runtime in application context so that // it is initialized only once. On console/winforms mode, e.g., from unit tests, ignore if( null != HttpContext.Current ) HttpContext.Current.Application["WorkflowRuntime"] = workflowRuntime;
return workflowRuntime; }
Workflow runtime is initialized from theApplication_Startevent inGlobal.asax. This ensures the initialization happens only once per application domain (see Example 4-33).
Example 4-33. Initializing WorkflowHelper from Global.asax
void Application_Start(object sender, EventArgs e) { // Code that runs on application startup
DashboardBusiness.WorkflowHelper.Init(); }
The runtime is disposed from theApplication_Endevent in Global.asax (see Example 4-34).
Example 4-34. Disposing the workflow runtime from Global.asax
void Application_End(object sender, EventArgs e) { // Code that runs on application shutdown DashboardBusiness.WorkflowHelper.Terminate(); }
Inside theWorkflowHelper, most of the work is done in theExecuteWorkflowfunction.DashboardFacadecalls this function to run a workflow, which:
Executes the workflow synchronously
Passes parameters to the workflow
Gets output parameters from the workflow and returns them
Handles exceptions raised in the workflow and passes to the ASP.NET exception handler
In the first step,ExecuteWorkflowcreates an instance of workflow and passes input parameters to it as shown in Example 4-35.
Example 4-35. ExecuteWorkflow function takes care of initializing workflow runtime and preparing a workflow for execution
public static void ExecuteWorkflow( Type workflowType, Dictionary<string,object>properties) { WorkflowRuntime workflowRuntime = Init();
ThenManualWorkflowSchedulerService service executes the workflow synchronously. Next, hook the workflow runtime’sWorkflowCompletedandWorkflowTerminatedevents to capture output parameters and exceptions and handle them properly, as shown in Example 4-36.
Example 4-36. Handle the workflow completion event to capture the output parameters from the workflow instance
EventHandler<WorkflowCompletedEventArgs>completedHandler = null; completedHandler = delegate(object o, WorkflowCompletedEventArgs e) { if (e.WorkflowInstance.InstanceId ==instance.InstanceId) { workflowRuntime.WorkflowCompleted -= completedHandler;
When the workflow completes,WorkflowCompletedEventArgsproduces theOutputParametersdictionary, which contains all of the workflow’s public properties. Next, read all of the entries inOutputParametersand update theInputParametersdictionary with the new values. This is required in theAddWidgetfunction ofDashboardFacade, where you need to know the widget instance created by the workflow.
WorkflowTerminatedfires when there’s an exception. When any activity inside the workflow raises an exception, this event fires and the workflow execution aborts. This exception is captured and thrown again so ASP.NET can trap it using its default exception handler, as shown in Example 4-37.
Example 4-37. Handle exceptions raised by the workflow runtime to find out whether there are any exceptions in a particular execution of a workflow instance
Exception x = null; EventHandler<WorkflowTerminatedEventArgs>terminatedHandler = null; terminatedHandler = delegate(object o, WorkflowTerminatedEventArgs e) { if (e.WorkflowInstance.InstanceId == instance.InstanceId) { workflowRuntime.WorkflowTerminated -= terminatedHandler; Debug.WriteLine( e.Exception );
This helps show exceptions in the ASP.NET exception handler. Exceptions thrown from workflow instances are captured and rethrown. As a result, they jump up to the ASP.NET exception handler, and you see the “yellow page of death” on your local computer (see Figure 4-9).
Figure 4-9. Handling exceptions in the workflow and escalating them so that they propagate to ASP.NET’s exception handler
Summary
In this chapter, you learned how to harness the power of LINQ to SQL to build a data access layer. You used Workflow Foundation to create a well-designed and well-implemented business layer. WF makes it easy for both architects and developers to be in sync during the design and implementation of an application, which leaves little room for developers to do anything outside the scope and functional requirements of the project. This saves time for architects, developers, and unit testers. In the next chapter, we will make some cool widgets that put the core to its practical use and delivers rich features to the users.