I.
Start page allows you to build your own homepage by dragging & term "content aggregators" & feel by utilizing Ajax and lots of advanced JavaScript & & Dropping widgets. You can completely personalize the page by putting what you want and where you want. You can add, remove widgets on your page. You can drag &s, implement a little interface and you are done! I have tried my best to create an architecture where you need not worry about Ajax and JavaScripts at all. Also the architecture allows you to use regular ASP.NET 2.0 controls, Ajax Control Toolkit controls and any extender in ASP.NET Ajax. You also get full server side programming supports Ajax Control Toolkit. Several custom extenders are used to provide the specialized drag & drop feature.
Middle tier is built using Windows Workflow foundation and the data access layer uses Dlinq and SQL Server 2005.
There's basically only one page, the Default.aspx. All the client side features you see are all available in Default.aspx and in the form of Widgets. We cannot do postback or too much navigation between pages because that would kill the Web 2.0ness. So, all the features must be provided in one page which never post back and does not redirect to other pages.
The tabs are just simple
<UL> and
<LI> inside an
UpdatePanel. When you change page title or add new page, it does not post back the whole page because only the
UpdatePanel contains the tab refreshes. Other part of the page remains as it is. first page that is rendered on screen.
Similarly on second visit, it just loads user's setup by executing
First four entries are the time taken by individual activities during data access. The time entries here are in seconds and the first four entries represent duration data access layer that generates really optimized SQL. If you have not used DLinq before, brace for impact!
When you use DLinq, you just design the database and then use SqlMetal.exe (comes database called "th rows:
var users = (from u in db.AspnetUsers select { UserId = u.UserId, UserName = u.LoweredUserName }).Skip(100).Take(20); foreach( var user in users ) { Debug.WriteLine( user.UserName ); }
If you are lookings sound like late binding. Actual Widget is also a server control which is loaded dynamically inside the widget container.
Each Widget contains several update panels which helps smaller part of the widgets get updated without whole page refresh existing HTML elements and creates new ones. As a result, all the extenders attached to the previous HTML elements get lost unless the extenders are also inside the
UpdatePanel. Putting extenders inside
UpdatePanel means whenever
UpdatePanel is refreshed, new instance of extenders are created and initialized. This makes UI experience very slow. You can actually see the slowness visually when you do something on the widget which makes it postback itself.
So, the final idea was to separate the header area and the body area between multiple
UpdatePanel. extender inside
UpdatePanel means every time the
UpdatePanel refreshes, the extender is created and initialized again. This gives poor performance.
So, the optimal solution so far is, have 2
UpdatePanel per
WidgetContainer, one contains convenient API for storing state, or changing the state of the widget like expanding/collapsing, etc.
WidgetContainer also conveys important messages to the actual widget like when it Widget definition. Then it puts the widget inside a body panel. It also passes its own reference as
IWidgetHost to the actual widget.
WidgetContainer implements.
All the functionality of
IWidgetHost was easy to implement except the
Close one. When
Close is called, we need to remove the widget from the page. This means, the
WidgetContainer on the page and the
WidgetInstance row in the database needs to be removed. Now this is something the
WidgetContainer itself cannot do. It needs to be done by the column container which contains
WidgetContainer. The Default.aspx is the container of all
WidgetContainers. So, whenever
Close is called,
WidgetContainer raises an event to the Default.aspx and Default.aspx does the actual work for removing the widget and refreshing the column.
Ajax Control Toolkit comes with a
DragPanel extender which you can use to provide drag & reorder list in each column and use the
DragPanel to drag the widgets. But I could not use
ReorderList because:
ReorderListstrictly uses
HTML Tableto render its items
ReorderListtakes Drag Handle template creates a drag handle for each item. We already have drag handle created inside a Widget, so we cannot allow
ReorderListto create another drag handle.
Next trouble was with the
DragPanel extender. The default implement of Drag & Drop in Ajax Control Toolkit has some problems:
So, I have made a
CustomDragDropExtender and a
CustomFloatingExtender.
CustomDragDropExtender is for the column containers where widgets are placed. It provides the reordering support. It allows any item right under the container to be ordered which is marked with a specific class name. Here's how it works:
 5
DIVs with the class named "
widget". It will allow reordering of only these five
divs:
 service and asynchronously inform server what just happened. Server updates the position of the widgets according to the new placement.
Note: I am not doing a postback, instead calling Web service on Drag & Drop. If I do postback, say postback the column
UpdatePanel, then the whole column will refresh which gives a poor drag & drop experience. This is why the drag &OPS style super slow JavaScript object model that ASP.NET Ajax provides.
It was so easy to build the data access layer using Dlinq. First I designed the database:
User contains a collection of pages. Each page contains a collection of
WidgetInstance.
WidgetInstance represents one Widget.
Widget table contains the definition of the widget, e.g. Name of the widget and the user control file name which has the code for the widget.
WidgetInstance represents an instance of a widget on a column and row of a page.
UserSetting stores some user level setting.
After designing the database, I used SqlMetal.exe and generated the data access class named
DashboardData which contains all the entity classes and Dlinq implementations for working with the database.
DashboardData inherits from
DataContext class which is a base class in
System.Data.Dlinq namespace for all data access classes. It has all the methods for insert, update, delete, select, transaction management, connection management, etc. business layer, you do not have access to the
DataContext which will be created by data access layer while updating the entity object. Business Layer will just send the entity object to the data access component and then the data access layer will do the update by creating a new
DataContext. But Dlinq requires you to attach the entity object "before" making changes to them. But regular business layer will make the modifications first and then send to data access component in order to update the object. So, a traditional attempt like this will fail: create
DataContext inside methods and somehow you need to store the reference to
DataContext between function calls. This might be ok "Attach first, modify later" requirement. Flickr website and then renders a 3X3 grid with the pictures.
First step is to download and parse the XML using Xlinq. Here's an easy way to prepare a
XElement from a URL:
var xroot = XElement.Load(url);
Now we convert each photo node inside the XML to an object of
PhotoInfo class for convenient processing:
PhotoInfo class from only those XML nodes which belong. Page index is changed when user clicks next or previous links. The
Skip method skips the number of items in the XML and the
Take method takes only the specified number of nodes from XML.
Once we have the photo objects to render, a 3X3 HTML
Table renders the photos:.
This was very easy to make using XLinq. Then I built the RSS Widget which shows RSS Feeds from a feed source. First I get the URL of the feed tweaking with the state. Each RSS Widget stores the URL in its State. The
Widget table has a
DefaultState column which contains predefined URL for RSS Widgets. When a RSS widget is created on the page, the default state is copied to the widget instance's state. XLinq makes it very easy to deal with the simple XML fragments. For example, here's how I read the URL:
public string Url { get { return State.Element("url").Value; } set { State.Element("url").Value = value; } }
The
state XML is like this:
<state> <count>3</count> <url>...</url> </state>
The
State property parses the XML and returns it as
XElement which refers to the root node
<state>:
private XElement State { get { if( _State == null ) _State = XElement.Parse(this._Host.GetState()); return _State; } }
Here's a workflow which shows what happens when a user visits the site:
First we get the
UserGuid from user's name. Then we use that
Guid to load pages, user setting and widgets in the current page. Finally we prepare a
UserPageSetup object which contains all the information required to render the page.
Now what happens when user visits the site for the first time? We need to create an anonymous user and create a default page setup for the user and then load workflow.
The activities do very small amount of work. For example,; }
The to execute 3 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.
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:
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. of this activity is very complex. Let's skip it.
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. 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 no cross app domain 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 heard divine revelation to me from 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 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. 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. first time load of Default.aspx or during tab switch, the widgets are setup by calling:.
So, why not just send "
true" and declare the function as
SetupWidgets(bool). Why go for the black art in Linq?:
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.
When a user first visits the site, an anonymous user setup is created. Now when the user decides to signup, we need to copy the page setups
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, 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
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 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.
The web project is a mix of WinFX, Lin Linq compiler so.
I generally remove some unnecessary
HttpModule from default ASP.NET pipeline for faster performance:
 it's slowness is so bad that you can visually see 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. 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
UpdatePanels. 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 3 extenders, the number of files downloaded is significant:
All the files with ScriptResource.axd are small scripts 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 get cached and thus do not download second time. So, you save significant download time on future visits.
So, final statement,
UpdatePanel 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 setcompile the whole site and then deploy the precompiled package.
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.s 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.
General
News
Question
Answer
Joke
Rant
Admin