Lessons Learned Developing DNN Modules

May 2, 2012

Jumping into DNN development, here’s a couple of things I learned from developing my first modules.

Module Path

There are a couple of variables a module can derive its file system location (and thus relative URL paths) from:

ControlPath /dnn/DesktopModules/MyModule/
ModulePath /dnn/DesktopModules/MyModule/
Request.ApplicationPath /dnn
Request.CurrentExecutionFilePath /dnn/Default.aspx
ModuleConfiguration.ControlSrc DesktopModules/MyModule/View.ascx

If you need to reference files in the file system, or URLs relative to the module’s installation path, this statement

var IncludePath = ModuleConfiguration.ControlSrc
  .Replace("/View.ascx", "/");

gives you the module’s base directory.

Packaging

You can freely edit the module’s .dnn file to edit the components of the installation.

For example, if the module has no Edit or Settings dialog, remove Edit.ascx* and/or Settings.ascx* from the .dnn file under

component/
  desktopModule/
    moduleDefinitions/
      moduleDefinition/
        moduleControls

as you remove or exclude them from the DNN project (.csproj file).

If you do not have an Edit form, disable the registration of the Edit form in the ModuleActions getter of View.ascx.cs.

For layout definition and CSS classes in your Edit.ascx, see the HTML\EditHtml.ascx that comes with DNN.

View/Edit Mode

If your View.ascx should behave differently depending on whether it is displaying in View mode or Edit mode, use the following markup to distinguish the two modes:

<%  if (!DotNetNuke.Common.Globals.IsEditMode())  {   %>
      <!-- markup for edit mode -->
<%  } else { %>
      <!-- markup for view mode -->
<%  } %>

Closing Forms

To close a dialog, simply redirect to the current page:

protected void cmdUpdate_Click(object sender, EventArgs e)
{
  try
  {
    UpdateSettings();
    Response.Redirect(Globals.NavigateURL(), true);
  }
  catch (Exception exc)
  {
    Exceptions.ProcessModuleLoadException(this, exc);
  }
}

JavaScript

DNN uses a combination of JavaScript libraries

Embedding DNN pages in iframes

A typical DNN page contains menu, header, content panes, and a footer. The default skin of DNN6 is called DarkKnight and provides a page skin called „Host: DarkKnight – popUpSkin“ with just a single content pane and no headers, footers, borders, etc., and is thus perfect for embedding DNN pages inside an <iframe> either in the same DNN installation or from outside.

Display Time Range

You can set the period to display a page using the Start Date and End Date settings in the Advanced Settings section of the Module Settings.
Start and End Dates are only settable on a day level, there is no built-in way to display modules based on time of day.

Automatic Refresh

Automatic Refresh is supported on page level, but not on module level.

A little warning: the refresh interval is also active in Edit mode, so if you set the interval too short, you may not be able to set it to a longer interval directly in the page. You need to navigate to Host/Page Management and reset the refresh interval.


Getting Started with DotNetNuke 6, Windows 7, and IIS7.5

April 27, 2012

I wrote about DotNetNuke as a web application earlier this year, but did not document how to get started developing DotNetNuke modules. Since a simple web search delivers unfiltered results and information on versions 3 thru 6, let’s catch up on the infrastructure needed for DotNetNuke 6::

  • Download the MSBuild Community Tasks, an extension to the VS build utility
  • Download DNN (New Install or VS Starter Kit) (I am working with 6.1.4, in the meantime 6.1.5 has been released)
  • Download the DNN Module Development Template and follow the installation instructions
  • Install the MSBuild tasks
  • Unzip DotNetNuke.zip into a new directory (under wwwroot or under your development directory)
  • Right click on the DNN directory and assign full control for the IIS_IUSRS user (this is the system user that IIS typically uses to access the file system)

In IIS Manager

  • create a web application pointing to the DNN directory
  • select the web application
  • open the .Net Compilation feature, and set Debug = true
  • in the Basic Settings dialog, make sure an Application Pool with .Net 2.0 Framework is assigned (Integrated Mode seems to work find)
  • select Deactivate Ping in Application Pool to avoid timeouts during debugging

The debugging-related settings are necessary to avoid the error message

The web server is not configured correctly. See help for common configuration errors. Running the web page outside of the debugger may provide further information.

when trying to debug from Visual Studio.

Use SQL Server or SQL Server Express as your database server and

  • make sure SQL Authentication is enabled
  • create a database for DNN
  • create a login for the DNN user
  • create a user in the DNN database with the newly created login
  • update the TWO connection strings in the DNN web.config file

Point your browser to the web application to make sure the settings are correct, and step thru the installation procedure of DNN.

Start Visual Studio (both 2008 and 2010 seem to work with the project template). To create a DNN module, execute:

  • Create project “DotNetNuke C# Compiled Module” inside DesktopModules directory of the DNN installation
  • Adjust project settings according to generated Documentation.html
  • See also the original blogs here and here describing the usage of the template.
  • Edit the project’s .dnn file (copyright, license, readme, etc.)
  • Implement the module’s functionality
  • Switch to Release mode for 1st deployment and Build
  • Build in release mode will generate two zip files in the packages directory, namely [modulename][version]_Install.zip and [modulename][version]_Setup.zip
  • In DNN, login as Host, select Host menu, Extensions, hover over the Manage button, start the Install Extension Wizard, and upload the Source.zip from the DesktopModules/MyModule/packages directory
  • Installation should not produce any error messages. Click Return.
  • Since the uploaded files overwrite the original source files, VS will alert you with a “project changed” message.

That’s it!

Modules can be assigned to categories, but this assignment is performed by the DNN administrator, not the developer.

  • In the Admin menu, select Taxonomy
  • Click the Edit icon on entry Module_Categories
  • Press ”Add Term” to add new module category, input the category name, and hit ”Create New Term”
  • Press “Update” to save
  • In Host/Extensions,
  • Click the Edit icon on module
  • Select the desired Module Category
  • Scroll down and press ”Update Extension”

Computer says “fakepath”

April 2, 2012

In a web application, we originally implemented the following functionality:

  • a user is allowed to upload files (Word documents, PDFs, etc)
  • if the uploaded file is from a network share (mapped drive), the mapped drive path needs to be translated into a UNC path
  • using the UNC path, a server component can check for changed file dates
  • if a changed file is detected, some workflow should be initiated

Files uploaded with IE (6 and 7) automatically included the path information of the file, whereas for Firefox 3, no file path was passed. This was worked around with a bit of JavaScript:

    ClientScript.RegisterClientScriptBlock(GetType(), "copy",
@"function copyName(){
    document.getElementById('" + edFullFilename.ClientID + "').value = 
        document.getElementById('" + edFilename.ClientID + @"').value;
}", true);
    edFilename.Attributes.Add("onkeyup", "copyName()");
    edFilename.Attributes.Add("onfocus", "copyName()");
    edFilename.Attributes.Add("onchange", "copyName()");

which essentially copied the original file path to a hidden input field.

As browsers are becoming more aware of security, and implement more and more HTML 5 features, all this changes.

IE8 started to introduce the c:\fakepath\ pseudo directory, and other browsers followed. As stated on the WHATWG mailing list,

The original plan was to just have the filename. Unfortunately, it turns out that if you do that, there are certain sites that break, because they expect the path (and they expect a Windows path, no less). This is why Opera and IE8 return a fake path — not because HTML5 says to do it. In fact I made HTML5 say it because they were doing it.

(I would expect Firefox, Safari, and Chrome to follow suit; Firefox for compatibility, and Safari and Chrome for privacy.)

For IE, there remains the solution to add the web server to the Trusted Internet Zone

Additionally, the “Include local directory path when uploading files” URLAction has been set to “Disable” for the Internet Zone. This change prevents leakage of potentially sensitive local file-system information to the Internet.

but we are looking for a generic cross-browser solution.

Probably it’s time to rethink the whole feature and make users copy+paste the file name rather than upload the file for such a scenario.


Fixing the AjaxControlToolkit ColorPicker

March 2, 2012

The Ajax Control Toolkit ColorPicker adds a dynamic color palette to an asp:TextBox and sets the textbox’s value to the hex value of the selected color.

There are two problems with the control (.Net 3.5 build 51116 of Nov 2011):

  • First, the OnClientColorSelectionChanged JS event is not fired if the user edits the textbox manually.

To fix this problem, you need to add two properties to the asp:TextBox declaration

onchange="javascript: colorTextChanged(this);" 
onkeyup="javascript: colorTextChanged(this);"

and implement a JavaScript function

function colorTextChanged(sender) {
    // sender is the textbox, sender.value is the edited color
}
  • Second, the color palette does not show if you set the SelectedColor property from code

The bug report I found on this topic is quite dated (Sep 2009), so nobody seems to care. The solution provided certainly worked for older versions of ACT, but needs to be adjusted (AjaxControlToolkit vs. Sys.Extended.UI) to look like this:

Sys.Application.add_init(function() {
    // Store the color validation Regex in a "static" object off of   
    // Sys.Extended.UI.ColorPickerBehavior.  If this _colorRegex object hasn't been   
    // created yet, initialize it for the first time.   
    if (!Sys.Extended.UI.ColorPickerBehavior._colorRegex) {
        Sys.Extended.UI.ColorPickerBehavior._colorRegex = 
            new RegExp('^[A-Fa-f0-9]{6}$');
    }
});  

 


Toggle Paging Mode in ASP.Net GridView

February 24, 2012

Customers requested the possibility to toggle the display of data in a GridView between paged view and complete view. I sketch a method to add a LinkButton in the Pager rows that performs this toggle.

A GridView’s pager row is rendered in HTML as

<tr>
  <td colspan="7">                   <-- the number of columns in the grid
    <table border="0">
      <tbody><tr>
        <td><span>1</span></td>
        <td><a href="javascript:__doPostBack('GridView1','Page$2')">2</a></td>
        ... etc for each page ...
      </tr></tbody>
    </table>
  </td>
</tr>

My approach is to insert the LinkButton right to the table containing the page selectors, by injecting a right-floating span before the page selector table in GridView_RowCreated:

protected void GridView_RowCreated(object sender, GridViewRowEventArgs e)
{
  var gv = sender as GridView;

  switch (e.Row.RowType)
  {
    case DataControlRowType.Pager:
      TableCell cell = e.Row.Cells[0];
      Table tbl = (Table)cell.Controls[0];
      cell = tbl.Rows[0].Cells[0];

      var span = new HtmlGenericControl("span");
      span.Attributes["style"] = "position:relative; float:right";
      span.Attributes["align"] = "right";
      span.Controls.Add(new Literal 
        { Text = "<table border=\"0\"><tbody><tr><td>" });
      LinkButton btnPaging = new LinkButton
        { Text = IsPaging(gv) ? "All Records" : "Paged View" };
      btnPaging.Click += new EventHandler(btnPaging_Click);
      span.Controls.Add(btnPaging);
      span.Controls.Add(new Literal 
        { Text = "</td></tr></tbody></table>" });
      cell.Parent.Parent.Parent.Controls.AddAt(0, span);
      break;

To find out which mode a GridView is rendering, we need a little helper function

bool IsPaging(GridView gv)
{
  return gv.PageSize <= 100;
}

Clicking the LinkButton will now toggle the paging mode, and we also save the PageSize to restore the previous value when clicking again:

void btnPaging_Click(object sender, EventArgs e)
{
  var gv = ((LinkButton)sender).Parent.Parent
    .Parent.Parent.Parent as GridView;
  if (IsPaging(gv))
  {
    gv.Attributes["pagesize"] = gv.PageSize.ToString();
    gv.PageSize = 100000;
    gv.PageIndex = 0;
  }
  else
  {
    gv.PageSize = deviolib.Convert.ToIntDef(gv.Attributes["pagesize"], 20);
  }
}

When I worked out this solution, I found that if the result displays in a single page, the pager is not displayed anymore. This answer on SO showed how to make it visible again: In the GridView_PreRender event, set both PagerRows’ Visible value.

void GridView_PreRender(object sender, EventArgs e)
{
  var gv = ((GridView)sender);

  GridViewRow pagerRow = gv.TopPagerRow;
  if (pagerRow != null && pagerRow.Visible == false)
    pagerRow.Visible = true;

  pagerRow = gv.BottomPagerRow;
  if (pagerRow != null && pagerRow.Visible == false)
    pagerRow.Visible = true;
}

Manipulating Request.QueryString

January 25, 2012

The Request.QueryString property is declared as System.Collections.Specialized.NameValueCollection but the internal implementation is really a derived (undocumented) class called System.Web.HttpValueCollection.

You can access each query string parameter using the index operator [], but you cannot modify the parameter values or add or delete a parameter from the collection:

var q = Request.QueryString;
// => read-only System.Web.HttpValueCollection
if (q["param"] == null)
  q.Add("param", "some-value");

This code will throw a System.NotSupportedException.

Why do you want to manipulate the query string anyway? Say you have a request and you want to add/change/remove a parameter of the query string, and then redirect to the modified URL. Of course, you want to make sure that each parameter is in the URL exactly once. Request.QueryString[] provides a simply mechanism to access and modify the parameters and their values.

To get a modifiable NameValueCollection from the current request, we need the helper method System.Web.HttpUtility.ParseQueryString (which again returns an HttpValueCollection):

var q = System.Web.HttpUtility.ParseQueryString(
    Request.QueryString.ToString());
if (q["param"] == null)
    q.Add("param", "some-value");
var r = Request.Path + "?" + q.ToString();
Response.Redirect(r);

To redirect from the current request, we take the request’s Path and concatenate the collection’s ToString() result. The HttpValueCollection.ToString() adds ‘&’ and ‘=’ between parameters and their values (as opposed to NamedValueCollection.ToString() which just returns the class name).


xcopy to c:\inetpub\wwwroot fails with “Access denied”

January 25, 2012

Rather than manually copying files from \\tsclient\some\path to c:\inetpub\wwwroot\webdir I wanted to write a small batch file using xcopy and /exclude to deploy web application files.

However, even when starting the .cmd from a command line in administrator mode, I received an “Access denied” message for each file to be copied due to User Account Control prohibiting write access.

As it turns out, the task can be successfully performed using robocopy with the /zb (or /b ?) switch. (Use robocopy /? to find the huge collection of switches.)

Consequently, the feature known as xcopy deployment should therefore be renamed to robocopy deployment ;)


Online Help and Computer-Based Training using DotNetNuke

January 12, 2012

A project that I am working on deals with DotNetNuke 6 as online help and/or Computer-based Training software (CBT) for an existing web application.

Both the application and DotNetNuke manage registered users and their privileges, and DNN handles the content management for the online help contents.

Note that a couple of years ago I wrote about Wikis for online help, but a Wiki (typically) does not allow for user-specific or role-specific content to be displayed, and was found as insufficient for this scenario.

The solution that we came up with was that both applications are synchronized via a custom Web Service that (mainly) matches the logins of both applications and logs application access.

Each application needed to be extended by a hyperlink mechanism that calculates the web address of the corresponding page in the other application (usually the landing page with some parameters), nicknamed “jumper” in the chart below, and the landing page itself which performs a login based on a session ID in the URL string, and redirects to the application or content page, also encoded in a URL parameter.

The DotNetNuke modules were developed using the DotNetNuke Module Development Template on CodePlex.


Deployment Checks for Configuration Data

January 10, 2012

Previous posts already dealt with the topic of checking configuration data stored in web.config and app.config.

Checking web.config on Application Start sketched how to use XmlDocument and XPath to check required entries in the <serviceModel> section used by web service clients.

Complex Data in .Net .config Files mentioned the built-in validators in the System.Configuration namespace as subclasses of ConfigurationValidatorAttribute. These validators are invoked as soon as the configuration section is accessed.

However, after deployment we also want to check other configuration data:

  • app.config, web.config (connection strings, etc.)
  • Properties.Settings sections
  • Configuration data stored in a database

The data to be checked should be accessible via an object of a typed class, such as the Properties.Settings class or a configuration class mirroring a database record.

We can then define a delegate to check whether an object’s property is a valid value (the example handles string values, but can be extended for any data type):

delegate string CheckFunction<T>(T data, 
    Expression<Func<T, string>> property);

A method handling the check and logging the result is defined as

void Log<T>(T data, string prefix, Expression<Func<T, string>> property, 
    CheckFunction<T> fn)
{
    var propertyName = prefix + "." + property.GetPropertyName();
    var propertyValue = property.Compile().Invoke(data);
    var errorMessage = fn(data, property);

    if (string.IsNullOrEmpty(errorMessage))
        LogResult(propertyName, propertyValue, "ok", true);
    else
        LogResult(propertyName, propertyValue, errorMessage, false);
}

with the LogResult() method defined as

void LogResult(string propertyName, string propertyValue, 
    string errorMessage, bool isValid);

and the GetPropertyName() extension method as previously defined.

Let’s say we want to check a .config setting identifying an existing directory, such as an upload directory for a web application:

var webconfig = Properties.Settings.Default;
Log(webconfig, "My.Namespace.Properties.Settings", 
    w => w.UploadTempPath, CheckDirectory);

The CheckDirectory method may be implemented like this:

string CheckDirectory<T>(T conf, Expression<Func<T, string>> property)
{
    string value = property.Compile().Invoke(conf);
    if (string.IsNullOrEmpty(value))
        return "null or empty";

    if (!Directory.Exists(value))
        return "directory does not exist";

    return null;
}

For data stored in the AppSettings section, we can define similar checks:

var appsettings = System.Configuration.ConfigurationManager.AppSettings;
Log(appsettings, "AppSettings", a => a["AppHelpLink"], CheckUrl);

with CheckUrl() defined as

string CheckUrl<T>(T conf, Expression<Func<T, string>> property)
{
    string value = property.Compile().Invoke(conf);
    if (string.IsNullOrEmpty(value))
        return "null or empty";

    try
    {
        Uri uri = new Uri(value);
    }
    catch(Exception ex)
    {
        return ex.GetType() + " " + ex.Message;
    }

    value = value.ToLower();
    if (!value.StartsWith("http://") && !value.StartsWith("https://"))
        return "not http:// or https://";
    return null;
}

Of course, these checks can be applied to any structured configuration data, and the check methods can be implemented as required by the system to be deployed.


ASP.Net 4 Application and Page Life Cycle (with Data Binding)

January 7, 2012

A recent pingback to my ASP.Net Page Life Cycle diagram reminded me of my plans to complete that diagram:

  • the data-binding events on controls and templates were missing
  • the Application object life cycle (read: Global.asax) had been completely left out

Time to update the diagram, and include data binding and application events. Right-click to save:


Follow

Get every new post delivered to your Inbox.