In previous "Cambrian First Look" blogs I have described the new Features Module. This is the most obvious new component in the initial Cambrian release, but it is not the only change. There have been quite a lot of changes/additions to the Framework in preparation for other promised Cambrian features. In this Blog I will start previewing these Framework changes.
The first major Framework change is mainly a refactoring of existing code. Pre-Cambrian the Container and Module injection was handled in two methods (Page_Init and InjectModule) of the Skin class (Skin.vb). When I started to work on the changes to Admin modules which I have described in an earlier blog I found it quite hard to decipher what was happening where, as the code for the InjectModule method was ~500 lines and the code for Page_Init was ~300 lines. It was also obvious that there was no clear "Separation of Concerns" - the Skin class was handling everything. In order to break the code into more managable chunks I refactored the code in these two methods introducing a new Class (Pane), as shown below.
Pre-Cambrian Skin and Container both existed (they represent the "base class" for all Skins and Containers), but there was no relationship between them. In Cambrian Skin now has a collection - Panes - of Pane objects, and Pane has a collection - Containers - of Container objects. This represents the actual physical heirarchy. A Skin designer places "Panes" in the skin, which can contain multiple modules - and each module has a Container wrapper. Each Pane object has a "PaneControl" property which represents the HtmlContainerControl (div, span, td and p tags) which is the actual physical Pane. Likewise each Container also has an HtmlContainerControl (ContentPane) which is used to contain the injected module. In the diagram, while the ContentPane and PaneControl both point at the same HtmlContainerControl, they do not actually represent the "same" control.
Why does this help?
This architecture helps because we can now off-load some of the processing to the appropriate class. Thus, Skin takes care of loading the Panes, it then parses the ActiveTab and passes the modules to the appropriate Pane. The Pane class manages loading the Container for each module and passes the module to the Container class, and finally the Container class "injects" the Module.
Lets look at Skin.vb's OnInit method
491 Protected Overrides Sub OnInit(ByVal e As System.EventArgs)
492 'Call base classes method
493 MyBase.OnInit(e)
494
495 Dim bSuccess As Boolean = True
496
497 'Load the Panes
498 LoadPanes()
499
500 'Load the Module Control(s)
501 If Not IsAdminControl() Then
502 ' master module
503 bSuccess = ProcessMasterModules()
504 Else
505 ' slave module
506 bSuccess = ProcessSlaveModule()
507 End If
508
509 'Load the Control Panel
510 InjectControlPanel()
511
512 'Process the Panes attributes
513 ProcessPanes()
514
515 'Register any error messages on the Skin
516 If Not Request.QueryString("error") Is Nothing Then
517 Skin.AddPageMessage(Me, CRITICAL_ERROR, Server.HtmlEncode(Request.QueryString("error")), _
518 UI.Skins.Controls.ModuleMessage.ModuleMessageType.RedError)
519 End If
520
521 If Not (PortalSecurity.IsPageAdmin()) Then
522 ' only display the warning to non-administrators (adminsitrators will see the errors)
523 If Not bSuccess Then
524 Skin.AddPageMessage(Me, MODULELOAD_WARNING, String.Format(MODULELOAD_WARNINGTEXT, PortalSettings.Email), _
525 UI.Skins.Controls.ModuleMessage.ModuleMessageType.YellowWarning)
526 End If
527 End If
528
529 End Sub
This method is much more manageable than the 300 lines in Page_Init. Partly this is due to just breaking the method up into smaller sections, but a large chunk of code has been offloaded to Pane and Container. The method is quite straightforward. First the Panes are loaded, next the Modules are loaded (either Master Modules or Slave Modules) then the ControlPanel is loaded and finally the Panes are processed (this sets things like adding the Pane borders in layout mode).
if we look at the LoadPanes method.
260 Private Sub LoadPanes()
261 Dim ctlControl As Control
262 Dim objPaneControl As HtmlContainerControl
263
264 ' iterate page controls
265 For Each ctlControl In Me.Controls
266 objPaneControl = TryCast(ctlControl, HtmlContainerControl)
267
268 'Panes must be runat=server controls so they have to have an ID
269 If objPaneControl IsNot Nothing AndAlso Not String.IsNullOrEmpty(objPaneControl.ID) Then
270 ' load the skin panes
271 Select Case objPaneControl.TagName.ToUpper
272 Case "TD", "DIV", "SPAN", "P"
273 ' content pane
274 If objPaneControl.ID.ToLower() <> "controlpanel" Then
275 'Add to the PortalSettings (for use in the Control Panel)
276 PortalSettings.ActiveTab.Panes.Add(objPaneControl.ID)
277
278 'Add to the Panes collection
279 Panes.Add(objPaneControl.ID, New Pane(objPaneControl))
280 Else
281 'Control Panel pane
282 _ControlPanel = objPaneControl
283 End If
284 End Select
285 End If
286 Next
287 End Sub
this code is very similar to the code that used to exist at the beginning of the Page_Init event handler.
837 Private Sub Page_Init(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Init
838 Dim objModules As New ModuleController
839 Dim objModule As ModuleInfo = Nothing
840 Dim ctlPane As Control
841 Dim blnLayoutMode As Boolean = Common.Globals.IsLayoutMode
842
843 Dim bSuccess As Boolean = True
844
845 ' iterate page controls
846 Dim ctlControl As Control
847 Dim objHtmlControl As HtmlControl
848 For Each ctlControl In Me.Controls
849 ' load the skin panes
850 If TypeOf ctlControl Is HtmlControl Then
851 objHtmlControl = CType(ctlControl, HtmlControl)
852 If Not objHtmlControl.ID Is Nothing Then
853 Select Case objHtmlControl.TagName.ToUpper
854 Case "TD", "DIV", "SPAN", "P"
855 ' content pane
856 If ctlControl.ID.ToLower() <> "controlpanel" Then
857 PortalSettings.ActiveTab.Panes.Add(ctlControl.ID)
858 End If
859 End Select
860 End If
861 End If
862 Next
the main difference being line 279 which creates the Pane object, passing a reference to the actual Pane control, and adding the new Pane object to the Panes collection.
In addition line 282 sets the Control Panel (this code used to be at the very end of Page_Init method but makes more sense to set the ControlPanel at the same time as the other Panes are set (the Control Panel is in effect a special kind of Pane).
ProcessMasterModules and ProcessSlaveModules are essentially the same code as the rest of the old Page_Init method, just refactored into two separate methods (Separation of Concerns). In each method ultimately there is code that Injects the module into the Pane. For example, the last few lines of ProcessSlaveModules are as follows.
468 ' verify that the current user has access to this control
469 If PortalSecurity.HasNecessaryPermission(slaveModule.ModuleControl.ControlType, _
470 PortalSettings, slaveModule) Then
471 'try to inject the module into the pane
472 bSuccess = InjectModule(pane, slaveModule)
473 Else
474 Response.Redirect(AccessDeniedURL(MODULEACCESS_ERROR), True)
475 End If
InjectModule is where most of the refactoring has occurred. This method used to be ~500 lines of code. In Cambrian, it is quite short as all it does is hand-off the processing to the appropriate Pane.
233 Private Function InjectModule(ByVal objPane As Pane, ByVal objModule As ModuleInfo) As Boolean
234 Dim bSuccess As Boolean = True
235
236 ' check if user has EDIT permissions for module
237 Dim blnHasModuleEditPermissions As Boolean = PortalSecurity.HasNecessaryPermission(SecurityAccessLevel.Edit, _
238 PortalSettings, objModule, _
239 UserController.GetCurrentUserInfo.Username)
240 If blnHasModuleEditPermissions = True AndAlso objModule.ModuleDefinition.DefaultCacheTime <> -1 Then
241 HasModuleEditPermission = True
242 End If
243
244 'try to inject the module into the pane
245 Try
246 objPane.InjectModule(objModule)
247 Catch ex As Exception
248 bSuccess = False
249 End Try
250
251 Return bSuccess
252 End Function
This method checks if the user has the appropriate permissions and then calls the Pane objects InjectModule method (handing it the Module).
Pane.vb's InjectModule method (see below) loads the relevant Container and passes it the Module (by setting its ModuleConfiguration property)
355 Public Sub InjectModule(ByVal objModule As ModuleInfo)
356 Dim bSuccess As Boolean = True
357
358 Try
359 If Not IsAdminControl() Then
360 ' inject an anchor tag to allow navigation to the module content
361 PaneControl.Controls.Add(New LiteralControl(" & objModule.ModuleID.ToString & """>"))
362 End If
363
364 'Load container control
365 Dim ctlContainer As DotNetNuke.UI.Containers.Container = LoadModuleContainer(objModule)
366
367 'Add Container to Dictionary
368 Containers.Add(ctlContainer.ID, ctlContainer)
369
370 'Attach Module to Container
371 ctlContainer.ModuleConfiguration = objModule
372
Setting the Containers ModuleConfiguration triggers the Container to complete the module injection.
106 Public Property ModuleConfiguration() As ModuleInfo
107 Get
108 Return _ModuleConfiguration
109 End Get
110 Set(ByVal value As ModuleInfo)
111 _ModuleConfiguration = value
112 ProcessModule()
113 End Set
114 End Property
In part 2 of this series of Blogs I will show how the Container completes the injection and introduce you to some new Interfaces and a new class.