DNN Community Blog

The Community Blog is a personal opinion of community members and by no means the official standpoint of DNN Corp or DNN Platform. This is a place to express personal thoughts about DNNPlatform, the community and its ecosystem. Do you have useful information that you would like to share with the DNN Community in a featured article or blog? If so, please contact .

The use of the Community Blog is covered by our Community Blog Guidelines - please read before commenting or posting.


Step by Step Webforms MVP and DotNetNuke – Part 2.

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:

  1. Right click the Models folder and select Add Class.
  2. Name the first class ViewWebformsMVPModel.
  3. Add the name space to the class.
  4. 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
  5. 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
  6. Add another class and call it EditWebformsMVPModel
  7. 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:

  1. Drag the edit and view ascx files into the Views folder.
  2. Right click the Views folder and add two interfaces, one called IViewWebformsMVP and the other called IEditWebformsMVP.
  3. 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
  4. 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
  5. 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
  6. Open the EditWebFormsMVP code behind and add the following bolded code:
       1:      Partial Class EditWebformsMVP
       2:          Inherits ModuleView(Of EditWebformsMVPModel)
       3:          Implements IEditWebFormsMVP
  7. 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:

  1. Right click the Presenters folder and add two classes, one called EditWebformsMVPPresenter and the other called ViewWebformsMVPPresenter.
  2. Add the name spaces.
  3. The presenter class needs to inherit from the core WebFormsMVP presenter class.
  4. 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
  5. 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.

  1. Cut the code inside the Page_Load event in the ViewWebFormsMVP.ascx.vb file.
  2. Paste it into the second constructor directly after the MyBase.New(_View) line of code.
  3. Change the lines of code that bind the List to the lstContent Datalist to assign the List to the View.Model.List.
  4. 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
  5. For now comment out the lstContent_ItemDataBound method, I’ll come back to that later.
  6. 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:

  1. Set the DataSource property in the mark up as below
    <asp:datalist id="lstContent"  DataSource='<%# Model.ListWebformsMVPInfo %>' 
           datakeyfield="ItemID" runat="server" cellpadding="4">
  2. 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
  3. 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="&lt;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.

Comments

Comment Form

Only registered users may post comments.

NewsArchives


Aderson Oliveira (15)
Alec Whittington (11)
Alex Shirley (10)
Andrew Nurse (30)
Andy Tryba (1)
Anthony Glenwright (5)
Antonio Chagoury (28)
Ash Prasad (32)
Ben Schmidt (1)
Benjamin Hermann (25)
Benoit Sarton (9)
Beth Firebaugh (12)
Bill Walker (36)
Bob Kruger (5)
Bogdan Litescu (1)
Brian Dukes (2)
Brice Snow (1)
Bruce Chapman (20)
Bryan Andrews (1)
cathal connolly (55)
Charles Nurse (163)
Chris Hammond (209)
Chris Paterra (55)
Clinton Patterson (40)
Cuong Dang (21)
Daniel Bartholomew (2)
Daniel Mettler (154)
Dave Buckner (2)
David Poindexter (4)
David Rodriguez (3)
Doug Howell (11)
Erik van Ballegoij (30)
Ernst Peter Tamminga (74)
Geoff Barlow (10)
George Alatrash (6)
Gifford Watkins (3)
Gilles Le Pigocher (3)
Ian Robinson (7)
Israel Martinez (17)
Jan Blomquist (2)
Jan Jonas (3)
Jaspreet Bhatia (1)
Jenni Merrifield (6)
Joe Brinkman (274)
John Mitchell (1)
Jon Henning (14)
Jonathan Sheely (4)
Jordan Coopersmith (1)
Joseph Craig (2)
Kan Ma (1)
Keivan Beigi (3)
Ken Grierson (10)
Kevin Schreiner (6)
Leigh Pointer (31)
Lorraine Young (60)
Malik Khan (1)
Matthias Schlomann (15)
Mauricio Márquez (5)
Michael Doxsey (7)
Michael Tobisch (3)
Michael Washington (202)
Miguel Gatmaytan (3)
Mike Horton (19)
Mitchel Sellers (28)
Nathan Rover (3)
Navin V Nagiah (14)
Néstor Sánchez (31)
Nik Kalyani (14)
Peter Donker (54)
Philip Beadle (135)
Philipp Becker (4)
Richard Dumas (22)
Robert J Collins (5)
Roger Selwyn (8)
Ruben Lopez (1)
Ryan Martinez (1)
Salar Golestanian (4)
Sanjay Mehrotra (9)
Scott McCulloch (1)
Scott S (11)
Scott Wilkinson (3)
Scott Willhite (97)
Sebastian Leupold (80)
Shaun Walker (237)
Shawn Mehaffie (17)
Stefan Cullmann (12)
Stefan Kamphuis (12)
Steve Fabian (31)
Timo Breumelhof (24)
Tony Henrich (3)
Torsten Weggen (2)
Vicenç Masanas (27)
Vincent Nguyen (3)
Vitaly Kozadayev (6)
Will Morgenweck (40)
Will Strohl (165)
William Severance (5)

Content Layout

Subscribe to DNN Digest

DNN Digest is our monthly email newsletter. It highlights news and content from around the DNN ecosystem, such as new modules and themes, messages from leadership, blog posts and notable tweets. Keep your finger on the pulse of the ecosystem by subscribing.  


Copyright 2017 by DNN Corp Terms of Use Privacy
What is Liquid Content?
Find Out
What is Liquid Content?
Find Out
What is Liquid Content?
Find Out