In Part 1 I set up the standard Starter Kit module to use interfaces and be more testable. In Part 2 I will implement the MVP Pattern using the Web Forms MVP project that is now included as part of DotNetNuke. The following code samples assume you are using DNN 5.4. There are a few changes from DNN 5.3 that make it easier to write tests and get around an issue with Windsor Castle’s problems with classes that implement more than one interface.
In Part 1 I listed the classes and interfaces we will need. The M part of MVP is for Model so the first class to add will be the model for the module. The model will store any properties you need such as whether the item is editable, the lsit of items and any other properties you will need to store for use in the presenter. In this way its a little like the View-Model used in MVVM.
Follow these steps to add the models for the Edit and View aspects of your module:
- Right click the Models folder and select Add Class.
- Name the first class ViewWebformsMVPModel.
- Add the name space to the class.
- Add a property to handle whether or not the Item in the list is editable by adding the following code
1: Private _IsEditable As Boolean
2: Public Property IsEditable() As Boolean
3: Get
4: Return _IsEditable
5: End Get
6: Set(ByVal value As Boolean)
7: _IsEditable = value
8: End Set
9: End Property
- Add another property to hold a List of the items
1: Private _ListWebformsMVPInfo As List(Of WebformsMVPInfo)
2: Public Property ListWebformsMVPInfo() As List(Of WebformsMVPInfo)
3: Get
4: Return _ListWebformsMVPInfo
5: End Get
6: Set(ByVal value As List(Of WebformsMVPInfo))
7: _ListWebformsMVPInfo = value
8: End Set
9: End Property
- Add another class and call it EditWebformsMVPModel
- Add a property to hold the item you are editing by adding the following code:
1: Private _Item As WebformsMVPInfo
2: Public Property Item() As WebformsMVPInfo
3: Get
4: Return _Item
5: End Get
6: Set(ByVal value As WebformsMVPInfo)
7: _Item = value
8: End Set
9: End Property
Thats the models done now the next letter in MVP is for View so I’ll now show you how to set up the views for the Edit and View screens. The view will have an interface that defines the events and any methods that are needed. The Interface for ViewWebFormsMVP is the simplest kind as it has no events or methods. The Interface for the edit screen will have events for the three buttons on it for Save, Cancel and Delete and also two methods to control the visibility of the Delete button and the audit control. To get the ascx controls to use the new interface you just implement in the code behind. Doing this allows you to test the model and presenter with a mock view.
Follow these steps to set up the Views:
- Drag the edit and view ascx files into the Views folder.
- Right click the Views folder and add two interfaces, one called IViewWebformsMVP and the other called IEditWebformsMVP.
- Add the following bolded code to the IViewWebformsMVP interface so that view knows how to find the model:
1: Imports DotNetNuke.Web.Mvp
2:
3: Namespace YourCompany.Modules.WebFormsMVP
4: Public Interface IViewViewWebformsMVP
5: Inherits IModuleView(Of ViewWebformsMVPModel)
6: End Interface
7:
8: End Namespace
- Add the following bolded code to IEditWebformsMVP
1: Imports DotNetNuke.Web.Mvp
2:
3: Namespace YourCompany.Modules.WebFormsMVP
4:
5: Public Interface IEditWebFormsMVP
6: Inherits IModuleView(Of EditWebformsMVPModel)
7: Sub ShowAuditControl(ByVal show As Boolean)
8: Sub ShowDeleteButton(ByVal show As Boolean)
9:
10: Event Cancel As EventHandler
11: Event Delete As EventHandler
12: Event Save As EventHandler(Of UIEventArgs(Of WebformsMVPInfo))
13:
14: End Interface
15:
16: End Namespace
- Open the code behind for ViewWebFormsMVP and add the following bolded code and delete the existing Inherits line:
1: Partial Class ViewWebformsMVP
2: Inherits ModuleView(Of ViewWebformsMVPModel)
3: Implements IViewWebformsMVP
4: Implements Entities.Modules.IActionable
- Open the EditWebFormsMVP code behind and add the following bolded code:
1: Partial Class EditWebformsMVP
2: Inherits ModuleView(Of EditWebformsMVPModel)
3: Implements IEditWebFormsMVP
- Implement the interface by adding the following code:
1: Public Sub ShowAuditControl(ByVal show As Boolean) Implements IEditWebFormsMVP.ShowAuditControl
2: Throw New NotImplementedException()
3: End Sub
4:
5: Public Sub ShowDeleteButton(ByVal show As Boolean) Implements IEditWebFormsMVP.ShowDeleteButton
6: Throw New NotImplementedException()
7: End Sub
8:
9: Public Event Cancel As EventHandler Implements IEditWebFormsMVP.Cancel
10: Public Event Delete As EventHandler Implements IEditWebFormsMVP.Delete
11: Public Event Save As EventHandler(Of UIEventArgs(Of WebformsMVPInfo)) Implements IEditWebFormsMVP.Save
Now the the views have their interfaces all wired up we still have a bit of work to do. You’ll notice that the code behind files still have lots of code in the events for the buttons and load etc. All of this code needs to be moved to the presenter classes as its code that needs to be tested. Then we need to change the events to raise the event from the interface so the presenter can do its job. To set up the presenter classes do the following:
- Right click the Presenters folder and add two classes, one called EditWebformsMVPPresenter and the other called ViewWebformsMVPPresenter.
- Add the name spaces.
- The presenter class needs to inherit from the core WebFormsMVP presenter class.
- The presenter also needs two constructors, add the following code in bold to set this up. The first constructor is used in production and the second one is sued for test which allows you to pass in a mock controller for testing.
1: Namespace YourCompany.Modules.WebformsMVP
2: Public Class ViewWebformsMVPPresenter
3: Inherits ModulePresenter(Of IViewWebformsMVP, ViewWebformsMVPModel)
4:
5: Public Sub New(ByVal _View As IViewWebformsMVP)
6: Me.New(_View, New WebformsMVPController(New SqlDataProvider()))
7: End Sub
8:
9: Public Sub New(ByVal _View As IViewWebformsMVP, ByVal _Controller As IWebformsMVPController)
10: MyBase.New(_View)
11: End Sub
12: End Class
13: End Namespace
- Do the same for the EditWebFormsMVPPresenter
1: Namespace YourCompany.Modules.WebformsMVP
2: Public Class EditWebformsMVPPresenter
3: Inherits ModulePresenter(Of IEditWebFormsMVP, EditWebformsMVPModel)
4:
5: Public Sub New(ByVal _View As IEditWebFormsMVP)
6: Me.New(_View, New WebformsMVPController(New SqlDataProvider()))
7: End Sub
8:
9: Public Sub New(ByVal _View As IEditWebFormsMVP, ByVal _Controller As IWebformsMVPController)
10: MyBase.New(_View)
11: End Sub
12: End Class
The classes are now set up so the next step is to move the logic code from the code behind files into the presenter classes. This is pretty much a Cut & Paste operation with a few changes to use the View rather than controls directly. The ViewWebFormsMVP is the simplest so let’s do that one first.
- Cut the code inside the Page_Load event in the ViewWebFormsMVP.ascx.vb file.
- Paste it into the second constructor directly after the MyBase.New(_View) line of code.
- Change the lines of code that bind the List to the lstContent Datalist to assign the List to the View.Model.List.
- Then change the ProcessModuleLoadException call to remove the Me from it as this is no longer needed as the base class puts that in. The code looks like this:
1: Public Sub New(ByVal _View As IViewWebformsMVP, ByVal _Controller As IWebformsMVPController)
2: MyBase.New(_View)
3: Try
4: Dim objWebformsMVPs As New WebformsMVPController
5: Dim colWebformsMVPs As List(Of WebformsMVPInfo)
6:
7: ' get the content from the WebformsMVP table
8: colWebformsMVPs = objWebformsMVPs.GetWebformsMVPs(ModuleId)
9:
10: If colWebformsMVPs.Count = 0 Then
11: ' add the content to the WebformsMVP table
12: Dim objWebformsMVP As WebformsMVPInfo = New WebformsMVPInfo
13: objWebformsMVP.ModuleId = ModuleId
14: objWebformsMVP.Content = Localization.GetString("DefaultContent", LocalResourceFile)
15: objWebformsMVP.CreatedByUser = Me.UserId
16: objWebformsMVPs.AddWebformsMVP(objWebformsMVP)
17: ' get the content from the WebformsMVP table
18: colWebformsMVPs = objWebformsMVPs.GetWebformsMVPs(ModuleId)
19: End If
20:
21: ' bind the content to the repeater
22: _View.Model.ListWebformsMVPInfo = colWebformsMVPs
23:
24: Catch exc As Exception 'Module failed to load
25: ProcessModuleLoadException(exc)
26: End Try
27: End Sub
- For now comment out the lstContent_ItemDataBound method, I’ll come back to that later.
- There is one last step to wire up the Vie to the Presenter. In the ViewWebformsMVP.ascx.vb file you need to add an attribute to the class to tell it which Presenter it should use. To do this add the bolded code to the class as shown below:
1: <PresenterBinding(GetType(ViewWebformsMVPPresenter))> _
2: Partial Class ViewWebformsMVP
3: Inherits ModuleView(Of ViewWebformsMVPModel)
Now that the Presenter takes care of all the logic and it has no knowledge of the controls that the values it works with should go in the ascx file needs to be updated to use some one way data binding. To do this set the DataSource property on the DataList control in the markup and then call Me.DataBind() in the Page_PreRender event. Foolow these steps to accomplish this:
- Set the DataSource property in the mark up as below
<asp:datalist id="lstContent" DataSource='<%# Model.ListWebformsMVPInfo %>'
datakeyfield="ItemID" runat="server" cellpadding="4">
- In the code behind file add the Page_PreRender event and tll the page to DataBind.
1: Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender
2: Me.DataBind()
3: End Sub
- Next is to modify the ascx a bit more to use the Model properties. The whole DataList looks like this: Notice the use of ModuleContext, this is available in the base class and gives access to most of the DNN properties.
1: <asp:datalist id="lstContent" DataSource='<%# Model.ListWebformsMVPInfo %>' datakeyfield="ItemID" runat="server" cellpadding="4">
2: <ItemTemplate>
3: <table cellpadding="4" width="100%">
4: <tr>
5: <td valign="top" width="100%" align="left">
6: <asp:HyperLink ID="HyperLink1" NavigateUrl='<%# ModuleContext.EditURL("ItemID",DataBinder.Eval(Container.DataItem,"ItemID")) %>'
7: Visible="<%# Model.IsEditable %>" runat="server">
8: <asp:Image ID="Image1" runat="server" ImageUrl="~/images/edit.gif" AlternateText="Edit"
9: Visible="<%#Model.IsEditable%>" resourcekey="Edit" /></asp:HyperLink>
10: <asp:Label ID="lblContent" runat="server" Text='<%# DataBinder.Eval(Container.DataItem,"DisplayContent") %>' CssClass="Normal" />
11: </td>
12: </tr>
13: </table>
14: </ItemTemplate>
15: </asp:datalist>
That's the ViewWebFormsMVP control finished. The refactoring is relatively simple because you just move the code from the code behind to the presenter and change any references directly to controls to use the property on the View.Model.
The process is the same for the EditWebFormsMVP control except that we have events to handle as well. I won’t show all the code here as there is a lot. I’ll show one event and how to pass the values from the View to the Presenter using the UIEventArgs. Firstly let’s look at the UIEventArgs class, it’s very simple, it a normal EventArgs class with a generic property that you can add any object to. In this example the generic will be the WebformsMVPInfo object.
1: Public Class UIEventArgs(Of T)
2: Inherits EventArgs
3:
4: Public Sub New(ByVal item As T)
5: itemValue = item
6: End Sub
7:
8: Private itemValue As T
9: Public Property Item() As T
10: Get
11: Return itemValue
12: End Get
13: Set(ByVal value As T)
14: itemValue = value
15: End Set
16: End Property
17:
18: End Class
In the Edit presenter I need to know the ItemId that is to be edited which is in the QueryString. To access the Request object you will need a refernce to System.Web.Abstractions. Add a property to the presenter for ItemId like so:
1: Public ReadOnly Property ItemId() As Long
2: Get
3: Dim _ItemId As Integer = Null.NullInteger
4: If Not String.IsNullOrEmpty(Request.Params("ItemId")) Then
5: _ItemId = Int32.Parse(Request.Params("ItemId"))
6: End If
7: Return _ItemId
8: End Get
9: End Property
In the ViewWebFormsMVPPresenter I added the code to populate the Model in the constructor, this works fine because I don’t need anything from the Request. However in the Edit presenter I need to know the ID of the item to be edited which is gathered from the QueryString therefore I can’t put the code in the constructor. Instead the code goes into an Override for OnLoad. Before I show you the code for that you will need to add a provate variable to hold the Controller class.
1: #Region "Private Members"
2: Private Controller As IWebformsMVPController
3: #End Region
The last thing to do before we add the OnLoad code is to add another constructor to the Info class so we can create a new one if the edit form is adding a new record.
1: Public Sub New(ByVal mId As Integer, ByVal uId As Integer)
2: ModuleId = mId
3: ItemId = Null.NullInteger
4: CreatedByUser = uId
5: CreatedDate = Now
6: End Sub
Add the OnLoad code now by adding the following code:
1: Protected Overrides Sub OnLoad()
2: MyBase.OnLoad()
3: Try
4: If Not DotNetNuke.Common.Utilities.Null.NullInteger = ItemId Then
5: ' get content
6: Dim objTestDrivenDNNModule As WebformsMVPInfo = Controller.GetWebformsMVP(ModuleId, ItemId)
7: If Not objTestDrivenDNNModule Is Nothing Then
8: View.Model.Item = objTestDrivenDNNModule
9: Else ' security violation attempt to access item not related to this Module
10: Response.Redirect(NavigateURL())
11: End If
12: Else
13: View.Model.Item = New WebformsMVPInfo(ModuleId, UserId)
14: View.ShowDeleteButton(False)
15: View.ShowAuditControl(False)
16: End If
17: Catch exc As Exception
18: ProcessModuleLoadException(exc)
19: End Try
20: End Sub
That takes care of loading the correct item into the View.Model.Item. As with the ViewWebFormsMVP control the ascx markup needs to be updated to use one way databinding. The change is very simple, just set the Text property to the Content property of the Item as shown in bold below:
1: <table width="650" cellspacing="0" cellpadding="0" border="0" summary="Edit Table">
2: <tr valign="top">
3: <td class="SubHead" width="125"><dnn:label id="lblContent" runat="server" controlname="lblContent" suffix=":"></dnn:label></td>
4: <td>
5: <dnn:texteditor id="txtContent" Text='<%# Model.Item.Content %>' runat="server" height="200" width="500" />
6: <asp:RequiredFieldValidator ID="valContent" resourcekey="valContent.ErrorMessage" ControlToValidate="txtContent"
7: CssClass="NormalRed" Display="Dynamic" ErrorMessage="<br>Content is required" Runat="server" />
8: </td>
9: </tr>
10: </table>
There are only a couple of things left to do now, create the methods in the presenter that will fire when the events are activated, wire up those methods in the constructor and show you how to use the UIEventArgs in the Save method. You’ll notice that the code is literally Cut & Pasted from the code behind.
Firstly add the following code to implement a method for the Cancel event:
1: Public Sub Cancel(ByVal sender As Object, ByVal e As EventArgs)
2: Response.Redirect(NavigateURL())
3: End Sub
Now implement the Delete method:
1: Public Sub Delete(ByVal sender As Object, ByVal e As EventArgs)
2: Try
3: ' Only attempt to delete the item if it exists already
4: Dim objTestDrivenDNNModule As WebformsMVPInfo = Controller.GetWebformsMVP(ModuleId, ItemId)
5: If Not objTestDrivenDNNModule Is Nothing Then
6: Controller.DeleteWebformsMVP(ModuleId, ItemId)
7: ' Redirect back to the portal home page
8: Response.Redirect(NavigateURL())
9: Else ' security violation attempt to access item not related to this Module
10: Response.Redirect(NavigateURL())
11: End If
12: Catch exc As Exception
13: 'Show a UI message later
14: ProcessModuleLoadException(exc)
15: End Try
16: End Sub
The Save method has a slightly different signature because it uses UIEventArgs. However the code is just cut pasted from the code behind as well.
1: Public Sub Save(ByVal sender As Object, ByVal e As UIEventArgs(Of WebformsMVPInfo))
2: Try
3: e.Item.CreatedByUser = UserId
4: e.Item.ModuleId = ModuleId
5: e.Item.ItemId = ItemId
6: If DotNetNuke.Common.Utilities.Null.NullInteger = ItemId Then
7: ' add the content
8: Controller.AddWebformsMVP(e.Item)
9: Else
10: ' update the content
11: Controller.UpdateWebformsMVP(e.Item)
12: End If
13: ' Redirect back to the portal home page
14: Response.Redirect(NavigateURL())
15: Catch exc As Exception 'Module failed to load
16: ProcessModuleLoadException(exc)
17: End Try
18: End Sub
That’s the methods created now wire them up in the constructor like so:
1: Public Sub New(ByVal _View As IEditWebFormsMVP, ByVal _Controller As IWebformsMVPController)
2: MyBase.New(_View)
3: Controller = _Controller
4: AddHandler View.Cancel, AddressOf Cancel
5: AddHandler View.Delete, AddressOf Delete
6: AddHandler View.Save, AddressOf Save
7: End Sub
Now change the code behind file so the events simple raise events for the presenter to listen to like so:
1: Private Sub cmdCancel_Click(ByVal sender As Object, ByVal e As EventArgs) Handles cmdCancel.Click
2: RaiseEvent Cancel(Me, e)
3: End Sub
4:
5: Private Sub cmdUpdate_Click(ByVal sender As Object, ByVal e As EventArgs) Handles cmdUpdate.Click
6: Model.Item = New WebformsMVPInfo()
7: Model.Item.Content = txtContent.Text
8: RaiseEvent Save(Me, New UIEventArgs(Of WebformsMVPInfo)(Model.Item))
9: End Sub
10:
11: Private Sub cmdDelete_Click(ByVal sender As Object, ByVal e As EventArgs) Handles cmdDelete.Click
12: RaiseEvent Delete(Me, e)
13: End Sub
The very last thing to do is implement the two methods that control the visbility of the delete button and audit control.
1: Public Sub ShowAuditControl(ByVal show As Boolean) Implements IEditWebFormsMVP.ShowAuditControl
2: ctlAudit.Visible = show
3: End Sub
4:
5: Public Sub ShowDeleteButton(ByVal show As Boolean) Implements IEditWebFormsMVP.ShowDeleteButton
6: cmdDelete.Visible = show
7: End Sub
Now build your project and, uh oh forgot to do something. In the Actions menu code you need to add ModuleContext to the front of the EditURL and GetNextActionID like so:
1: Public ReadOnly Property ModuleActions() As Entities.Modules.Actions.ModuleActionCollection
Implements Entities.Modules.IActionable.ModuleActions
2: Get
3: Dim Actions As New Entities.Modules.Actions.ModuleActionCollection
4: Actions.Add(ModuleContext.GetNextActionID,
Localization.GetString(Entities.Modules.Actions.ModuleActionType.AddContent, LocalResourceFile),
Entities.Modules.Actions.ModuleActionType.AddContent, "", "", ModuleContext.EditUrl(), False,
Security.SecurityAccessLevel.Edit, True, False)
5: Return Actions
6: End Get
7: End Property
Now its all go :) Your module is ready to rock and be tested which I’ll cover in Part 3. The solution thus far is available here.