Title:       Module Installer
Author:      Michael Washington 
Email:       webmaster@adefwebserver.com
Member ID:   12345
Language:    C# 2.0 etc
Platform:    Windows, .NET 3.5 
Technology:  ASP.NET
Level:       Beginner, Intermediate, Advanced
Description: A framework that allows you to upload modules into a web application, to enable an end-user to easily update and enhance it. 
Section      
SubSection   
License:     BSD

Why use a Module Installer?

One of the best things about DotNetNuke is it's ability to allow you to simply upload a .zip file that contains a "module package" and instantly add features to your website. If you need a Message Board for your website, you simply upload it and configure it. You don't have to create the code yourself. You upload code that someone else created and it will simply work.

The SilverlightDesktop.net project needed a method to allow developers to create modules for the framework, we copied the proven DotNetNuke method. We have posted the code here as a non-Silverlight application because we felt the code may be useful to others.

The Application

The application uses code from another CodeProject application we created, A Web based application configuration wizard, that provides a wizard to allow you to easily configure the application. We later used this code in the SilverlightDesktop.net project.

First, you create a sql database:

Configure the Web application:

Navigate to the website and configure the application using the installer:

After the application is set-up, click on the link to navigate to the Module Administration.

Upload the HelloWorldModule.zip file (the link to download this is at the top of this article):

After clicking the Continue link the page will refresh and the module will be configured.

The HelloWorld module will display in the drop-down list on the main page, and clicking the Load Module button will dynamically inject the module into the page and display it's contents.

The Module Package

The exploration of the installation process begins with the module package. This is a .zip file containing the program, database scripts (.sql scripts) and configuration information:

The files are zipped in the relative position they will need to be placed in when they are installed. The HelloWorldModule.dll file needs to be placed in the "bin" directory so it is placed in the "bin" directory when it is zipped up. The remaining files will be placed in the applications root directory. Note, the .sql and .config files will be deleted after installation and will not remain in the application.

The Module.config file

The module.config file has the following format:

<SilverlightDesktop>
<configversion>1.0</configversion>
<configtype>Module</configtype>
<modulename>HelloWorld</modulename>
<description>A simple module that says Hello World</description>
<assembly>HelloWorld.dll</assembly>
<version>01.00.00</version>
<removefiles>
<file>
<name>HelloWorld.ascx</name>
</file>
<file>
<name>HelloWorld.ascx.cs</name>
</file>
<file>
<name>bin\HelloWorldModule.dll</name>
</file>
</removefiles>
</SilverlightDesktop>

The Installer

The ModuleAdmin.aspx.cs file contains all the logic for installing the module. First a temporary directory is created and the .zip file is unzipped into that directory. SharpZipLib is used to unzip the file.

string strZipFilename = File1.PostedFile.FileName;
strZipFilename = System.IO.Path.GetFileName(strZipFilename);
File1.PostedFile.SaveAs(strTempDirectory + strZipFilename);

UploadMessage.Add(String.Format("File saved to {0}", strTempDirectory + strZipFilename));
UnzipFile(strTempDirectory + strZipFilename);

Next, Linq to XML is used to read the configuration file and to build a list of possible files to remove:

// Load the config file
XElement doc = XElement.Load(strTempDirectory + @"\Module.config");

string strconfigversion = doc.Element("configversion").Value;
string strconfigtype = doc.Element("configtype").Value;
string strmodulename = doc.Element("modulename").Value;
string strdescription = doc.Element("description").Value;
string strassembly = doc.Element("assembly").Value;
string strversion = doc.Element("version").Value;

// Build a list of files to remove
List<string> colFilesToRemove = new List<string>();
foreach (XElement Element in doc.Element("removefiles").Elements())
{
 colFilesToRemove.Add(Element.Value);
}

colFilesToRemove.Sort();

Linq to SQL is used determine if the module version is appropriate:

            // Get the current module version if any
            int intCurrentModuleVersion = 0;
            var result = from Module in DataClassesDataContext.Modules
                         where Module.ModuleName.ToLower() == strmodulename.ToLower()
                         select Module.ModuleVersion;

            intCurrentModuleVersion = (result.FirstOrDefault().ToString() == "") ? intCurrentModuleVersion : result.FirstOrDefault();

            int intModuleVersion = Convert.ToInt32(strversion.Replace(".", ""));

            if (intModuleVersion <= intCurrentModuleVersion)
            {
                UploadMessage.Add(String.Format("Current module version is {0}. Installing module version is {1}. 
			Aborting installation.", intCurrentModuleVersion.ToString(), intModuleVersion.ToString()));
                lbUploadMessage.DataSource = UploadMessage;
                lbUploadMessage.DataBind();
                return;
                // Exit
            }
            else
            {
                UploadMessage.Add(String.Format("Current module version is {0}. Installing module version {1}.", intCurrentModuleVersion.ToString(), 
			intModuleVersion.ToString()));
            }

Based on the current module version number (if any) the appropriate .sql scripts are located and executed. The Linq to SQL method, "ExecuteCommand" allows you to pass a .sql string that will be executed against the currently configured data source.

            // Get a list of all .sql scripts
            List colSQLScripts = Directory.GetFiles(strTempDirectory, "*.sql").ToList();
            colSQLScripts.Sort();

            foreach (string strFile in colSQLScripts)
            {
                string strFileName = Path.GetFileNameWithoutExtension(strFile);
                if (strFileName.ToLower() != "uninstall")
                {
                    int intVersion = Convert.ToInt32(strFileName.Replace(".", ""));
                    if (intVersion <= intModuleVersion)
                    {
                        try
                        {
                            string strSqlScript = GetSQLScript(strFile);
                            DataClassesDataContext.ExecuteCommand(strSqlScript);
                            File.Delete(strFile);
                            UploadMessage.Add(String.Format("SQL Script processed: {0}", strFileName));
                        }
                        catch (Exception ex)
                        {
                            UploadMessage.Add(String.Format("SQL Script error in script: {0} - {1}", strFileName, ex.ToString()));
                            lbUploadMessage.DataSource = UploadMessage;
                            lbUploadMessage.DataBind();
                            return;
                        }
                    }
                }
            }

The modules table is updated:

            // Delete record if it exists
            Module ModuleEntry = (from Module in DataClassesDataContext.Modules
                                  where Module.ModuleName.ToLower() == strmodulename.ToLower()
                                  select Module).FirstOrDefault();

            // // If the Module entry does not already exist, create it
            if (ModuleEntry == null)
            {
                ModuleEntry = new Module();
            }

            ModuleEntry.AssemblyName = strmodulename;
            ModuleEntry.ModuleDescription = strdescription;
            ModuleEntry.ModuleName = strmodulename;
            ModuleEntry.ModuleVersion = Convert.ToInt32(strversion.Replace(".", ""));

            //Read and insert the uninstall script
            if (File.Exists(strTempDirectory + "uninstall.sql"))
            {
                string strUninstall = GetSQLScript(strTempDirectory + "uninstall.sql");
                ModuleEntry.uninstall = strUninstall;
            }

            // If the Module entry does not already exist insert it
            if (ModuleEntry.ModuleID == 0)
            {
                DataClassesDataContext.Modules.InsertOnSubmit(ModuleEntry);
                UploadMessage.Add(String.Format("Created Module entry {0}", strmodulename));
            }

            DataClassesDataContext.SubmitChanges();

Unneeded files, processed files that will not be part of the installed package, are cleaned up:

            //Delete files
            foreach (string strDeleteFile in colFilesToRemove)
            {
                File.Delete(strTempDirectory.Replace(@"\Temp", "") + strDeleteFile);
                UploadMessage.Add(String.Format("Removed File: {0}", strDeleteFile));
            }

            //Delete the .zip, .config and uninstall files
            File.Delete(strTempDirectory + strZipFilename);
            File.Delete(strTempDirectory + "uninstall.sql");
            File.Delete(strTempDirectory + "Module.config");

            //Delete any file details in the database
            var colModuleFiles = from ModuleFiles in DataClassesDataContext.ModuleFiles
                                 where ModuleFiles.ModuleName.ToLower() == strmodulename.ToLower()
                                 select ModuleFiles;

            DataClassesDataContext.ModuleFiles.DeleteAllOnSubmit(colModuleFiles);
            DataClassesDataContext.SubmitChanges();

The remaining files are added to the ModuleFiles table (so they can be deleted when the module is uninstalled) and moved to their proper location:

            //Add The Module File information to the database
            List colDirectories = Directory.GetDirectories(strTempDirectory).ToList();
            colDirectories.Add(strTempDirectory);

            foreach (string strDirectory in colDirectories)
            {
                List colFiles = Directory.GetFiles(strDirectory).ToList();
                foreach (string strFile in colFiles)
                {
                    ModuleFile objModuleFile = new ModuleFile();
                    objModuleFile.ModuleName = strmodulename.ToLower();
                    objModuleFile.FileNameAndPath = strDirectory.Replace(strTempDirectory, "") + @"\" + Path.GetFileName(strFile);
                    DataClassesDataContext.ModuleFiles.InsertOnSubmit(objModuleFile);
                    DataClassesDataContext.SubmitChanges();

                    // Move the file to it's destination
                    File.Move(strFile, strFile.Replace(@"\Temp", ""));
                }
            }

Just use DotNetNuke

If you desire the functionality of an extensible framework you should simply use DotNetNuke. However, when that is not an option you may find that this code can provide a method to allow an end-user to easily update and enhance your application.

Implementing a framework such as this not only allows you to provide an easy way to implement enhancements, but it also allows others the opportunity to "extend" your application. This enhances the value of your application for your end-users.

Back to:

DotNetNuke® Module Development Help Website