public class DashboardFacade
{
private string _UserName;
public DashboardFacade( string userName )
{
this._UserName = userName;
}
public UserPageSetup NewUserVisit( )
{
var properties = new Dictionary<string,object>();
properties.Add("UserName", this._UserName);
var userSetup = new UserPageSetup();
properties.Add("UserPageSetup", userSetup);
WorkflowHelper.ExecuteWorkflow(
typeof( NewUserSetupWorkflow ), properties );
return userSetup;
}
There were 3 major headaches I had to solve while implementing the business layer using Workflow and Dlinq.
- Synchronous execution of workflow in ASP.NET
- Getting objects out of workflow after execution is complete
- Invoke one workflow from another synchronously
Synchronous execution of workflow in ASP.NET
Workflow is generally made for asynchronous execution. WorflowRuntime
is usually created only once per Application Domain and the same instance of runtime is used everywhere in the same app domain. In ASP.NET, the only way you can ensure single instance of a WorkflowRuntime
and make it available every where is by storing it in the HttpApplication
. Also you cannot use the Default scheduler service which executes workflows asynchronously. You need to use ManualWorkflowSchedulerService
which is specially made for synchronous workflow execution.
There's a handy class called WorkflowHelper
which does Workflow creation and execution. Its ExecuteWorkflow
function executes a workflow synchronously.
public static void ExecuteWorkflow( Type workflowType,
Dictionary<string,object> properties)
{
WorkflowRuntime workflowRuntime =
HttpContext.Current.Application["WorkflowRuntime"] as
WorkflowRuntime;
ManualWorkflowSchedulerService manualScheduler =
workflowRuntime.GetService
<ManualWorkflowSchedulerService>();
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(workflowType, properties);
instance.Start();
manualScheduler.RunWorkflow(instance.InstanceId);
}
It takes the type of workflow to execute and a dictionary of data to pass to the workflow.
Before running any workflow, first WorkflowRuntime
needs to be initialized once and only once. This is done in the Global.asax
in Application_Start
event.
void Application_Start(object sender, EventArgs e)
{
// Code that runs on application startup
DashboardBusiness.WorkflowHelper.Init();
}
The WorkflowHelper.Init
does the initialization work:
public static WorkflowRuntime Init()
{
var workflowRuntime = new WorkflowRuntime();
var manualService = new ManualWorkflowSchedulerService();
workflowRuntime.AddService(manualService);
var syncCallService = new Activities.CallWorkflowService();
workflowRuntime.AddService(syncCallService);
workflowRuntime.StartRuntime();
HttpContext.Current.Application["WorkflowRuntime"] = workflowRuntime;
return workflowRuntime;
}
Here you see two services are added to the workflow runtime. One is for synchronous execution and another is for synchronous execution of one workflow from another.
Invoke one workflow from another synchronously
This was a major headache to solve. The InvokeWorkflow
activity which comes with Workflow Foundation executes a workflow asynchronously. So, if you are calling a workflow from ASP.NET which in turn calls another workflow, the second workflow is going to be terminated prematurely instead of executing completely. The reason is, ManualWorkflowSchedulerService
will execute the first workflow synchronously and then finish the workflow execution and return. If you use InvokeWorkflow
activity in order to run another workflow from the first workflow, it will start on another thread and it will not get enough time to execute completely before the parent workflow ends.
Here you see only one activity in the second workflow gets the chance to execute. The remaining two activities do not get called at all.
Luckily I found an implementation of synchronous workflow execution at:
http://www.masteringbiztalk.com/blogs/jon/PermaLink,guid,7be9fb53-0ddf-4633-b358-01c3e9999088.aspx
It's an activity which takes the workflow as input and executes it synchronously. The implementation of this Activity is very complex. Let's skip it.
Getting objects out of workflow after execution is complete
This one was the hardest one. The usual method for getting data out of workflow is to use the CallExternalMethod
activity. You can pass an interface while calling a workflow and the activities inside the workflow can call host back via the interface. The caller can implement the interface and get the data out of the workflow.
It is a requirement that the interface must use intrinsic data types or types which are serializable. Serializable is a requirement because the workflow can go to sleep or get persisted and restored later on. But, Dlinq entity classes cannot be made serializable. The classes that SqlMetal generates are first of all not marked as [Serializable]
. Even if you add the attribute manually, it won't work. I believe during compilation, the classes are compiled into some other runtime class which does not get the Serializable
attribute. As a result, you cannot pass Dlinq entity classes from activity to workflow host.
The workaround I found was to pass object references as properties in the dictionary that we pass to workflow. As ManualWorkflowSchedulerService
runs the workflow synchronously, the object references remain valid during the lifetime or the workflow. There is not cross app domain call here, so there is no need for serialization. Also modifying the objects or using them does not cause any performance problem because the objects are allocated in the same process.
Here's an example:
public UserPageSetup NewUserVisit( )
{
var properties = new Dictionary<string,object>();
properties.Add("UserName", this._UserName);
var userSetup = new UserPageSetup();
properties.Add("UserPageSetup", userSetup);
WorkflowHelper.ExecuteWorkflow( typeof( NewUserSetupWorkflow ), properties );
return userSetup;
}
So far so good. But how do you write Dlinq code in a WinFX project? If you create a WinFX project and start writing Linq code, it won't compile. Linq requires special compiler in order to generate C# 2.0 IL out of Linq code. There's a specialized C# compiler in "C:\Program Files\Linq Preview\bin" folder which MSBuild uses in order to compile the Linq codes. After long struggle and comparison between a Linq project file and a WinFX project file, I found that WinFX project has a node at the end:
<Import Project="$(MSBuildExtensionsPath)\Microsoft\Windows Workflow Foundation\v3.0\ Workflow.Targets" />
And Linq project has the node:
<Import Project="$(ProgramFiles)\LINQ Preview\Misc\Linq.targets" />
These notes select the right MSBuild script. for building the projects. But if you just put Linq node in a WinFX project, it does not work. You have to comment out the first node:
<!--<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.Targets" />-->
After this, it built the code and everything successfully ran.
But workflows with Conditions and Rules did not run. At runtime, the workflows threw "Workflow Validation Exception". When I use code in the rule, it works. But if I use Declarative Rules in condition, then it does not work. Declarative rules are added as Embedded Resource under the workflow or activity which contains all the rules defined in an Xml format. It appears that the .rules file does not get properly embedded and workflow runtime cannot find it while executing a workflow.
Now this was a dead end for me. If I create regular WinFX project, then it works fine. But then again, I cannot write Linq code in a regular WinFX project. So, I have to create mix of Linq and WinFX project and use no Declarative rules. But I so desperately wanted to write rules in workflows and activities. I struggled whole night on this problem but found no solution. It was so frustrating. Then in the dawn, when there was absolute silence everywhere and the sun was about to rise, I heard divine revelation to me from the heaven:
Thou shalt bring forth the source of misery above thy
So, I did. I brought the .rules file (source of misery) from under the .cs file to one level upward on the project level. It then looked like this:
For this, I had to open the Project file (.csproj) in notepad and remove the <DependentUpon>
node under the <EmbeddedResource>
node:
<ItemGroup> <EmbeddedResource Include="Activities\CreateDeafultWidgetsOnPageActivity.rules"> <!-- <DependentNode>CreateDeafultWidgetsOnPageActivity.cs</DependentNode> --> </EmbeddedResource>
And it worked! There's absolutely no way in the world I could have known that, right?
Day 6: Page switch problem
Widgets need to know whether it's first time load of the widget or is it a postback. Normally when it's a first time load, widgets load all settings from their persisted state and render the UI for the first time. Upon postback, widgets don't restore settings from persisted state always, instead sometimes they update state or reflect small changes on the UI. So, it is important for user's to know when they are being rendered for the first time and when it is a postback.
However, when you have multiple tabs, the definition of first time load and postback changes. When you click on another tab, it's a regular postback for ASP.NET because a LinkButton
gets clicked. This makes the Tab UpdatePanel
postback asynchronously and on the server side we find out which tab is clicked. Then we load the widgets on the newly selected tab. But when widgets load, they call Page.IsPostBack
and they get true. So, widgets assume they are already on the screen and try to do partial rendering or try to access ViewState
. But this is not true because they did not appear on screen yet and there's no ViewState
for the controls on the widget. As a result, the widgets behave abnormally and all ViewState
access fails.
So, we need to make sure during tab switch, although it's a regular ASP.NET postback, Widgets must not see it as postback. The idea is to inform. widgets whether it is a first time load or not via the IWidget
interface.
On Default.aspx, there's a function SetupWidgets
which creates the WidgetContainer
and loads the widgets. Here's how it works:
private void SetupWidgets(Func<WidgetInstance, bool> isWidgetFirstLoad)
{
var setup = Context.Items[typeof(UserPageSetup)] as UserPageSetup;
var columnPanels = new Panel[] {
WidgetViewUpdatePanel.FindControl("LeftPanel") as Panel,
WidgetViewUpdatePanel.FindControl("MiddlePanel") as Panel,
WidgetViewUpdatePanel.FindControl("RightPanel") as Panel};
// Clear existin widgets if any
foreach( Panel panel in columnPanels )
{
List<WidgetContainer> widgets =
panel.Controls.OfType<WidgetContainer>().ToList();
foreach( var widget in widgets ) panel.Controls.Remove( widget );
}
Skip the Func<>
thing for a while. First, I clear the columns which contains the WidgetContainer
so that we can create the widgets again. See the cool Linq way to find out only the WidgetContainer
controls from the Panel
's Controls collection.
Now, we create the WidgetContainers
for the widgets on the newly selected tab:
foreach( WidgetInstance instance in setup.WidgetInstances )
{
var panel = columnPanels[instance.ColumnNo];
var widget = LoadControl(WIDGET_CONTAINER) as WidgetContainer;
widget.ID = "WidgetContainer" + instance.Id.ToString();
widget.IsFirstLoad = isWidgetFirstLoad(instance);
widget.WidgetInstance = instance;
widget.Deleted +=
new Action<WidgetInstance>(widget_Deleted);
panel.Controls.Add(widget);
}
While creating, we set a public property IsFirstLoad
of the WidgetContainer
in order to let it know whether it is being loaded for the first or not. So, during first time load of Default.aspx or during tab switch, the widgets are setup by calling:
SetupWidgets( p => true );
What you see here is called Predicate
. This is a new feature in Linq. You can make such predicates and avoid creating delegates and the complex coding model for delegates. The predicate returns true for all widget instances and thus all widget instances see it as first time load.
So, why not just send "true" and declare the function as SetupWidgets(bool)
. Why go for the black art in Linq?
Here's a scenario which left me no choice but to do this. When a new widget is added on the page, it is a first time loading experience for the newly added widget, but it's a regular postback for existing widgets already on the page. So, if we pass true or false for all widgets, then the newly added widget will see it as a postback just like all other existing widgets on the page and thus fail to load properly. We need to make sure it's a non-postback experience only for the newly added widget but a postback experience for the existing widget. See how it can be easily done using this Predicate
feature:
new DashboardFacade(Profile.UserName).AddWidget( widgetId );
this.SetupWidgets(wi => wi.Id == widgetId);
Here the predicate only returns true for the new WidgetId
, but returns false for existing WidgetId
.
Day 7: Signup
When user first visits the site, an anonymous user setup is created. Now when user decides to signup, we need to copy the page setups and all user related settings to the newly signed up user.
The difficulty was to get the anonymous user's Guid. I tried Membership.GetUser()
passing Profile.UserName
which contains the anonymous user name. But it does not work. It seems Membership.GetUser
only returns a user object which exists in aspnet_membership
table. For anonymous users, there's no row in aspnet_membership
table, only in aspnet_users
and aspnet_profile
tables. So, although you get the user name from Profile.UserName
, but you cannot use any of the methods in Membership
class.
The only way to do it is to read the UserId
directly from aspnet_users
table. Here's how:
AspnetUser anonUser = db.AspnetUsers.Single( u =>
u.LoweredUserName == this._UserName
&& u.ApplicationId == DatabaseHelper.ApplicationGuid );
Note: You must use LoweredUserName
, not the UserName
field and must include ApplicationID
in the clause. Aspnet_users
table has index on ApplicationID
and LoweredUserName
. So, if you do not include AapplicationID
in the criteria and do not use the LoweredUserName
field, the index will not hit and the query will end up in a table scan which is very expensive. Please see my blog post for details on this:
Careful-when-querying-on-aspnet
Once we have the UserId
of the anonymous user, we just need to update the UserID
column in Page
and UserSetting
table to the newly registered user's UserId
.
So, first get the new and old UserId
:
MembershipUser newUser = Membership.GetUser(email);
// Get the User Id for the anonymous user from the aspnet_users table
AspnetUser anonUser = db.AspnetUsers.Single( u =>
u.LoweredUserName == this._UserName
&& u.ApplicationId == DatabaseHelper.ApplicationGuid );
Guid ldGuid = anonUser.UserId;
Guid newGuid = (Guid)newUser.ProviderUserKey;
Now update the UserId
field of the Pages of the user:
List<Page> pages = db.Pages.Where( p => p.UserId == oldGuid ).ToList();
foreach( Page page in pages )
page.UserId = newGuid;
But here's a catch. You cannot change the field value if it's a primary key using Dlinq. You have to delete the old row using the old primary key and then create a new row using new primary key:
UserSetting setting = db.UserSettings.Single( u => u.UserId == oldGuid );
db.UserSettings.Remove(setting);
setting.UserId = newGuid;
db.UserSettings.Add(setting);
See DashboardFacade.RegisterAs(string email)
for the full code.
Web.config walkthrough
The web project is a mix of WinFX, Linq and ASP.NET Ajax. So, the web.config needs to be configured in such a way that it allows harmonious co-existence of these volatile technologies. The web.config itself requires a lot of explanation. I will just highlight the areas which are important.
You need to use Linq compiler so that default C# 2.0 compiler does not compile the site. This is done by:
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs"
type="Microsoft.CSharp.CSharp3CodeProvider,
CSharp3CodeDomProvider"/>
</compilers>
</system.codedom>
Then you need to put some extra attributes in the <compilation> node:
<compilation debug="true" strict="false" explicit="true">
Now you need to include the ASP.NET Ajax assemblies and WinFX assemblies:
<compilation debug="true" strict="false" explicit="true">
<assemblies>
<add assembly="System.Web.Extensions, ..."/>
<add assembly="System.Web.Extensions.Design, ..."/>
<add assembly="System.Workflow.Activities, ..."/>
<add assembly="System.Workflow.ComponentModel, ..."/>
<add assembly="System.Workflow.Runtime, ..."/>
You also need to put "CSharp3CodeDomProvider.dll" in the "bin" folder and add reference to System.Data.DLinq
, System.Data.Extensions
, System.Query
and System.Xml.Xlinq
. All these are required for Linq.
I generally remove some unnecessary HttpModule
from default ASP.NET pipeline for faster performance:
<httpModules>
<!-- Remove unnecessary Http Modules for faster pipeline -->
<remove name="Session"/>
<remove name="WindowsAuthentication"/>
<remove name="PassportAuthentication"/>
<remove name="UrlAuthorization"/>
<remove name="FileAuthorization"/>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, ..."/>
</httpModules>
How slow is ASP.NET Ajax
Very slow, especially in IE6. In fact it's slowness is so bad that you can visually see on local machine while running on your powerful development computer. Try pressing F5 several times on a page where all the required data for the page are already cached on the server side. You will see the total time it takes to fully load the page is quite long. ASP.NET Ajax provides a very rich object oriented programming model and a strong architecture which comes at high price on performance. From what I have seen, as soon as you put UpdatePanels on the page and some extenders, the page becomes too slow. If you just stick to core framework for only web service call, you are fine. But as soon as you start using UpdatePanel
and some extenders, it's pretty bad. ASP.NET Ajax performance is good enough for simple pages which has say one UpdatePanel
and one or two extenders for some funky effects. May be one more data grid on the page or some data entry form. But that's all that gives acceptable performance. If you want to make a start page like website where one page contains almost 90% of the functionality of the whole website, the page gets heavily loaded with javascripts generated by extenders and UpdatePanel
s. So, Start Page is not something that you should make using UpdatePanel
and Extenders. You can of course use the core framework without doubt for webservice calls, XML HTTP, login/logout, profile access etc.
Update: Scott Guthrie showed me that changing debug="false" in web.config emits much lither runtime scripts to client side and all the validation gets turned off. This result in fast javascript. execution for the extenders and update panel. You can see the real performance from the hosted site right now. The performance is quite good after this. IE 7, FF and Opera 9 shows much better performance. But IE 6 is still quite slow, but not as slow as it was before with debug="true" in web.config.
When you make a start page, it is absolutely crucial that you minimize network roundtrip as much as possible. If you study Pageflakes, you will see on first time load, the Wizard is visible right after 100KB of data transfer. Once the wizard is there, rest of the code and content download in the background. But if you close the browser and visit again, you will see total data transfer over the network is around 10 KB to 15 KB. Pageflakes also combines multiple smaller scripts and stylesheets into one big file so that the number of connection to the server is reduced and overall download time is less than large number smaller files. You really need to optimize to this level in order to ensure people feel comfortable using the start page everyday. Although this is a very unusual requirement, but this is something you should try in all Ajax applications because Ajax applications are full of client side code. Unfortunately you cannot achieve this using ASP.NET Ajax unless you do serious hacking. You will see that even for a very simple page setup which has only 3 extenders, the number of files downloaded is significant:
All the files with ScriptResource.axd are small scripts in Ajax Control Toolkit and my extenders. The size you see here is after gzip compression and they are still quite high. For example, the first two are nearly 100 KB. Also all these are individual requests to the server which could be combined into one JS file and served in one connection. This would result in better compression and much less download time. Generally each request has 200ms of network roundtrip overhead which is the time it takes for the request to reach server and then first byte of the response to return to client. So, you are adding 200ms for each connection for nothing. It is quite apparent to ScriptManager which scripts are needed for the page on server side because it generates all the script. references. So, if it could combine them into one request and serve them gzipped, it could save significant download time. For example, here 12 X 200ms = 2400ms = 2.4 sec is being wasted on the network.
However, one good thing is that, all of these gets cached and thus does not download second time. So, you save significant download time on future visits.
So, final statement, UpdatePanel
and Extenders are not good for websites which push client side richness to the extreme like Ajax Start Pages, but definitely very handy for not so extreme websites. It's very productive to have designer support in Visual Studio and very good ASP.NET 2.0 integration. It will save you from building an Ajax framework from scratch and all the javascript. controls and effects. In Pageflakes, we realized there was no point building a core Ajax framework from scratch and we decided to use Atlas runtime for XmlHttp and Webservice call. Besides the core Ajax stuff, everything else is homemade including the drag & drop, expand/collapse, fly in/out etc. These are just too slow using UpdatePanel
and Extenders. Both Speed & smoothness are very important to start pages because they are set as browser homepage.
Deployment Problem
Due to a problem in ASP.NET Ajax RC version, you can't just copy the website to a production server and run it. You will see none of the scripts are loading becuase ScriptHandler malfunctions. In order to deploy it, you will have to use the "Publish Website" option to precompile the whole site and then deploy the precompiled package.
How to run the code
- Install .NET 3.0 Framework
- Install ASP.NET Ajax RC1
- Install Linq May CTP
- Visual Studio 2005 Extensions for .NET Framework 3.0 (Windows Workflow Foundation)
- Restore the database in your SQL Server as "Dashboard" or whatever name you like.
- Open the web.config in "src\Dashboard" folder and specify correct connection string according to your SQL server configuration
- Load the Dashboard.sln and run.
Remember, you cannot just copy the website to a server and run it. It will not run. Something wrong with ScriptResource
handler in ASP.NET Ajax RC version. You will have to Publish the website and copy the precompiled site to a server.
Next Steps
If you like this project, let's make some cool widgets for it. For example, a To-do-list, Address book, Mail Widget etc. This can become a really useful start page if we can make some useful widgets for it. We can also try making a widget which runs Google IG modules or Pageflakes' flakes on it.
Conclusion
Ajax Start Page is a really complex project where you push DHTML & Javascript. to their limits. As you add more and more features on the client side, the complexity of the web application increases geometrically. Fortunately ASP.NET Ajax takes away a lot of complexity on the client side so that you can focus on your core features and leave the framework and Ajax stuffs to the runtime. Moreover, Dlinq and cool new features in .NET 3.0 makes it a lot easier to build powerful data access layer and business logic layer. Making all these new technologies work with each other was surely a great challenge and rewarding experience.
Shameless disclaimer: I am co-founder & CTO of Pageflakes, the coolest Web 2.0 Ajax Start Page. I like building Ajax websites and I am really, really good at it.
About the Author
Omar Al Zabir |