In this june blog we invite you to visit one of the website that we are using with Gilles to test the Store module, at http://beta5.dotnetnuke.fr.
This is an experimental - but fonctionnal - admin panel for Store, which is entirely built upon Entity framework and Dynamic Data.
At first sight this may seem like any module you get used to with DotNetNuke.
But a closer look will reveal some interesting and early technologies that are not so common.
Specifications
http://beta5.dotnetnuke.fr is a DNN 5.1 public beta instance, with Store module 2.1.17 beta (which is also a public beta that you can download at www.dotnetnuke.fr). This website runs on the framework 3.5 SP1.
This demo makes use of the Entity framework data model class library 'StoreModel.dll', which I built and explained in my previous blogs.
Guided tour
The Store tab contains a standard 2.1.17 Store module for a fictional horse riding equipement shop.
This is not the focus of this blog, and I suggest that you go straight to the experimental administrative section of this shop, which you will find under the Dynamic Store Admin main menu entry. The current demo tab is StoreDynamicData4. This may change any time, as this is a work in progress. I will fix some issues that you will notice during the tour.
Localization
The auto-generated nature of the scaffold does not make it easy to use DotNetNuke localization resource files. I will not discuss this here. For this demo, I have just tried to ensure that the french words are similar enough to their english counterparts, so it should not be too difficult to guess their meaning when necessary.
Entities.
This page displays an auto-generated list of the tables that are part of the Store module (they are called 'entities' in ORM jargon). For this simple demo, our entities map our database tables 1:1, but it might be different with a more sophisticated model. For example, we might have applied filter to the model to create an entity of non-deleted clothes in stock, and have this entity displayed in our list as if it was a regular table.
Dim visibleTables As System.Collections.IList = MetaModel.Default.VisibleTables
Menu1.DataSource = visibleTables
Menu1.DataBind()
As you may guess, VisiblesTables is the collection of tables we want to display, amoung those in the StoreModel. For these tests, we wanted to see all tables that are specific to Store. But you may also notice that the latest table is the standard DNN Users table, which is Store-related, but not Store-specific.
Navigation
Let's select either one of the first two tables, StoreCategories or StoreProducts, and have have a look at their properties.
What's interesting is the additional fields which appear after the standard properties of the entity. If you are in list mode, these 'navigational properties' are the latest columns to the right. It may be easier to see them after clicking 'Details', down the detail page.
These extra fiels are links to the related tables, i.e. the category, a review or a cart if you select a product; or Products if you selected a category.
Navigating from a table to the related tables is the essence or entity framework, to the point that the related content in the other table is called a 'navigation property' - kind of an extended property of the entity itself.
The navigation properties work with one-to-one, one-to-many or many-to-many relations. Also note that the Categories entities have a navigation property to their own table. This is because of the hierarchical structure or the sub-categories.
Scaffolding
The notion of scaffolding refers to the automated construction of the CRUD operations (I discovered scaffodling with Ruby On Rails, where the pages for listing, inserting, updating or deleting products would have been 2 words of code : 'scaffold StoreProducts')
There is a major difference beetween RoR's scaffold and Dynamic Data's scaffold though. Due to the interpreted nature of Ruby, RoR builds the scaffold classes on the fly. Scaffolding in Ruby On Rails refers to both the construction of the necessary classes from the database, and the basic generation of the HTML and the code (M, V and C).
With Entity framework and Dynamic Data, these operations are layered. The classes are baked into the StoreModel, and dynamic data is only responsible for the page generation.
You probably noticed that Dynamic Data includes a nice filter component (on top of the lists), and a pager. Both are accessible classes that can be configured.
Data access
There is not a single line of code refering to our database. The only visible layer from Dynamic Data is our Store model, which is contained in our bin/StoreModel.dll. Had we built our model against a non MS-SQL database, we would still have the exact same code. Here is the only line that refers to my StoreModel :
model.RegisterContext(GetType(StoreDynamicData4.StoreDynamicEntities), New ContextConfiguration() With {.ScaffoldAllTables = True})
Actions
Our dynamic data module contains 4 mains controls that are responsible of the standard CRUD operations :
list.ascx
details.ascx
edit.ascx
insert.ascx
As you can see, no single piece of code is dedicated to a table in particular. Pages are basically common to every entities, there is a page per action rather than a page per table.
It is possible to override this common behavior with CustomPages. All there is to do is create a custom version of any of the controls above, and place them in a folder that takes his name after the customized table
Altough my customized list.ascx for the StoreProducts is not very different from the others, you can notice minor specific design changes :
I just copied the standard list.ascx, changed it, and put it into :
CustomPages/Store_Products/List.aspx
A likely scenario would be to give it its own stylesheet.
Metadata
Not only is our model aware of the relations beetween tables, but it also knows the field types from the metadata. This is why we did not have to write any code to choose the proper control for our fields. Dynamic data contains a set of field templates, which are displayed as needed. Let's have a look at the Boolean.ascx.vb which handles the display of all boolean fields :
Imports System.Web.DynamicData
Partial Class BooleanField
Inherits System.Web.DynamicData.FieldTemplateUserControl
Public Overrides ReadOnly Property DataControl As Control
Get
Return CheckBox1
End Get
End Property
Protected Overrides Sub OnDataBinding(ByVal e As EventArgs)
MyBase.OnDataBinding(e)
Dim val As Object = FieldValue
If (Not (val) Is Nothing) Then
CheckBox1.Checked = CType(val,Boolean)
End If
End Sub
End Class
Routing
Dynamic Data uses a kind of MVC routing architecture that does not mix well with the existing DotNetNuke routing system.
First of all, the Dynamic Data project template that comes with Visual Studio routes the actions to Pages instead of Controls. Out of the box you get list.aspx, details.aspx, edit.aspx and insert.aspx instead of controls. Something that's not cool to make a DotNetNuke module.
Then Dynamic Data, pretty much like RoR, has its own convention for the file folders organisation, such as DynamicData/Content/Images or DynamicData/CustomPages.Store_Products/List.ascx
We configure the path to this structure in global.asax.vb :
model.DynamicDataFolderVirtualPath = "~/DesktopModules/StoreDynamicData4/DynamicData"
Now we have to define the routing. By default, this routing changes urls like :
StoreProducts/List or StoreProducts/details/xxxx
to the actual webform that will embed the actions controls. This is normally done with a route definition like :
routes.Add(New DynamicDataRoute("{table}/{action}.aspx") With { _
.Constraints = New RouteValueDictionary(New With {.Action = "List|Details|Edit|Insert"}), _
.Model = model})
Private Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
RegisterRoutes(RouteTable.Routes)
End Sub
But this routing assumes that you want these urls be routed to different pages according to the requested action. This is not what we want, since only Controls (.ascx) can be insertied into a a DotNetNuke module. For this reason, I tried to change the actions pages (.aspx) to actions controls (.ascx), and define the routing to redirect all URL's to the same default.aspx page (not yet the DNN default.aspx, but at least a unique page, assuming that if we could obtain a fully fonctionnal Dynamic Data with a single form, then it should be theorically possible to consider an integration into a pure DotNetNuke module later).
Using Dynamic Data with a single page seems to require a custom route handler in global.asax :
routes.Add(new DynamicDataRoute("{table}/{action}.aspx") With { _
.Constraints = new RouteValueDictionary(New With { .Action = "List|Details|Edit|Insert" }), _
.Model = model, _
.viewName ="MaViewName", _
.RouteHandler = new StoreDynamicDataRouteHandler() })
Public Class StoreDynamicDataRouteHandler
Inherits DynamicDataRouteHandler
Public Overrides Function CreateHandler(ByVal route As System.Web.DynamicData.DynamicDataRoute, _
ByVal table As System.Web.DynamicData.MetaTable, _
ByVal action As String) _
As System.Web.IHttpHandler
dim mapage as system.web.ui.page
dim monhandle as IHttpHandler
mapage = BuildManager.CreateInstanceFromVirtualPath( "~/DesktopModules/StoreDynamicData4/Default.aspx", GetType(Page))
monhandle = DirectCast(mapage, IHttpHandler)
return DirectCast(monhandle, IHttpHandler)
Public Class StoreDynamicDataRoute
Inherits DynamicDataRoute
Sub New(ByRef url As String)
MyBase.New(url)
Me.RouteHandler = New StoreDynamicDataRouteHandler()
End Sub
End Class
Now that all our action requests are redirected to the same page, how does it know what to do ? Fortunately, the context has kept track of our desired action, and we'll retrieve this information before we load the proper control (default.aspx.vb) :
Dim requestContext = DynamicDataRouteHandler.GetRequestContext(Context)
Dim action As String = requestContext.RouteData.GetRequiredString("action")
' Load the proper user control for the table/action
Dim ucVirtualPath As String ' replaces : = table.GetScaffoldPageVirtualPath(action)
Select Case action
Case "List" : ucVirtualPath = "DynamicData/PageTemplates/list.ascx"
Case "Edit" : ucVirtualPath = "DynamicData/PageTemplates/Edit.ascx"
Case "Details" : ucVirtualPath = "DynamicData/PageTemplates/Details.ascx"
Case "Insert" : ucVirtualPath = "DynamicData/PageTemplates/Insert.ascx"
Case Else
Throw New InvalidOperationException("Bug : action [" & action & "] differente de List, Edit, Details, ou Insert")
End Select
ph.Controls.Add(LoadControl(ucVirtualPath))
We are now one step away from integrating Dynamic Data into a pure DotNetNuke module.
Conclusion:
As you can see, using Dynamic Data to leverage the power of Entity Framework, is a very promessing perspective, but it is difficult to integrate nicely in a DotNetNuke website.
If you want to use it as an external project, aimed at the administrators, then it is quite straightforward.
But due to the conflicting natures of DNN's and Dynamic Data's routing architectures, I have not been able to make it a real module, and I could only insert it into an iframe, with it's own default.aspx page.
The iframe is a simple technique, and we get an excellent iframe module with DotNetNuke. Inserting your Dynamic Data project into an iframe is an elegant solution - tough highly politically incorrect. I'm not so sure why we may have dozens of opened windows on our screens, but it is prohibited to have 2 forms in a single page, if we want to remain XHTML compliant.
That said, let's remember that Entity Framework is not only a easy way to build quick scaffoldings. It's a rich framework upon which you can build you own tools. This Dynamic Data demo was just an example of how we can make use of the StoreModel, but it can and will also serve many other purposes.
My customised <%=Table.Displayname%>