Исходники к статье
Версия DNN на момент выхода статьи - DotNetNuke Community Edition 05.06.01
   Периодически требуется создать веб сайты за короткий срок, с каким-то небольшим функционалом или вообще без оного – так называемый «Сайт-Визитка», и чтобы после развертывания было можно просто забыть о нем, предоставив пользователям максимум возможностей - добавлять и удалять страницы, изменять контент и дизайн.
   В качестве CMS, для таких задач, я использую 
Dot Net Nuke, которая построена на технологии Microsoft Asp.Net . С ее помощью можно предоставить все вышеперечисленные возможности и много больше.  Так же, благодаря огромному сообществу, можно покупать и продавать модули и шкурки на 
snowcovered.com .В данной статье представлены только базовые сведения о создании модулей, с точки зрения ASP.net разработчика.
   Если за дело берется разработчик мало знакомый с DNN, то он знает, как написать требуемый функционал, но внедрить его в приложение будет проблематично. Далее мы как раз и пойдем по предложенному пути: определим задачу, создадим веб приложение, а после на его основе создадим установочный пакет модуля.
Задача:
   Требуется написать DNN модуль, который будет при заходе пользователя на страницу отображать его аватар и произвольную надпись под ним, если же аватар и надпись не задана, то отображать значения по умолчанию.
Проект:
   Создадим новый проект Asp.Net Empty Web Application с названием MyAvatar. Далее добавим класс Avatar.cs который будет представлять сущность аватара, вот его код:
namespace MyAvatar
{
   internal class Avatar
   {       
       public int UserId { get; set; }
       public byte[] Img{ get; set; }
       private string _text = "none";
       public string Text
       {
           get { return _text; }
           set { _text = value; }
       }
       private bool _isNewEntry = true;
       public bool IsNewEntry
       {
           get { return _isNewEntry; }
           set { _isNewEntry = value; }
       }
       public string ImageUrl
       {
           get
           {
               string url =  @"~/noAvatar.jpg";
               if (Img != null) url = string.Format(@"~/Image.aspx?id={0}", UserId);
               return url;
           }
       }
   }
}
   Свойства: UserId – уникально идентифицирует каждого пользователя, Img – картинка аватара для пользователя, Text – подпись под аватаром, IsNewEntry – определяет новая ли это запись или данные восстановлены из базы данных, ImageUrl – предоставляет ссылку на картинку аватара.
   Обратим внимание на свойство ImageUrl, в нем определен файл noAvatar.jpg, который нужно добавить к проекту в корневой каталог (взять можно из исходников), а так же указана страница Image.aspx. В данном случае, в академических целях, проще хранить картинки в базе, чем работать с файловой системой,  данная страница предоставляет картинки из базы по id.
   Создайте базу данных с именем MyAvatarDB. Из предыдущего листинга видно, что для хранения состояния модуля достаточно трех полей   UserId, Text, Img, остальные являются вычисляемыми. Далее создадим таблицу и CRUD процедуры, листинг:
USE MyAvatarDB
GO
CREATE TABLE MyAvatarTable(
UserId int NOT NULL PRIMARY KEY,
Text nvarchar(50) NOT NULL,
Image varbinary(max) NULL)
GO
CREATE PROCEDURE CreateMyAvatar
@UserId int,
@Text nvarchar(50),
@Image varbinary(max) = NULL
AS
BEGIN
INSERT MyAvatarTable (UserId,Text,Image)
VALUES (@UserId,@Text,@Image)
END
GO
CREATE PROCEDURE DeleteMyAvatar
@UserId int
AS
BEGIN
DELETE MyAvatarTable
WHERE UserId = @UserId
END
GO
CREATE PROCEDURE GetMyAvatarByUserId
@UserId int
AS
BEGIN
SELECT * FROM MyAvatarTable
WHERE UserId = @UserId
END
GO
CREATE PROCEDURE UpdateMyAvatar
@UserId int,
@Text nvarchar(50),
@Image varbinary(max) = NULL
AS
BEGIN
UPDATE MyAvatarTable
SET Text = @Text, Image =  @Image
WHERE UserId = @UserId
END
GO
Теперь определим строку подключения к базе данных в файле web.config:
<connectionstrings>
  <add name="MyAvatarConString" connectionstring="Data Source=.\SQLExpress;Initial Catalog=MyAvatarDB;Integrated Security=SSPI;">
</add>
</connectionstrings>
* This source code was highlighted with Source Code Highlighter.
   Добавим в проект новый класс, который будет являться репозиторием с именем AvatarRepository.cs. код:
using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
namespace MyAvatar
{
   internal static class AvatarRepository
   {
       private static SqlConnection BuildConnection()
       {
           return new SqlConnection(ConfigurationManager.ConnectionStrings["MyAvatarConString"].ConnectionString);
       }
       public static Avatar GetById(int userId)
       {
           Avatar avatar = new Avatar { UserId = userId };
           using (SqlConnection conn = BuildConnection())
           {
               SqlCommand command = conn.CreateCommand();
               command.CommandText = "GetMyAvatarByUserId";
               command.CommandType = CommandType.StoredProcedure;
               command.Parameters.AddWithValue("UserId", userId);
               conn.Open();
               var result = command.ExecuteReader();
               result.Read();
               if (result.HasRows)
               {
                   avatar.IsNewEntry = false;
                   avatar.Text = result["Text"].ToString();
                   if (result["Image"].GetType() != typeof(DBNull))
                       avatar.Img = (byte[])result["Image"];
                   conn.Close();
               }
           }
           return avatar;
       }
       public static void SaveOrCreate(Avatar avatar)
       {
           using (SqlConnection conn = BuildConnection())
           { 
               SqlCommand command = conn.CreateCommand();
               command.CommandText = (avatar.IsNewEntry ? "CreateMyAvatar" : "UpdateMyAvatar");
               command.CommandType = CommandType.StoredProcedure;
               command.Parameters.AddWithValue("UserId", avatar.UserId);
               command.Parameters.AddWithValue("Text", avatar.Text);
               command.Parameters.AddWithValue("Image", avatar.Img);
               conn.Open();
               command.ExecuteNonQuery();
               conn.Close();
           }
       }
       public static void DeleteById(int id)
       {
           using (SqlConnection conn = BuildConnection())
           {
               SqlCommand command = conn.CreateCommand();
               command.CommandText = "DeleteMyAvatar";
               command.CommandType = CommandType.StoredProcedure;
               command.Parameters.AddWithValue("UserId", id);
               conn.Open();
               command.ExecuteNonQuery();
               conn.Close();
           }
       }
   }
}
   Единственное на что следует обратить внимание – при вызове метода GetById изначально создается новый экземпляр класса аватар и при его создании свойство IsNewEntry принимает значение true. После чего происходит попытка чтения данных из базы, и при успешной попытке данному свойству выставляется значение false, после чего, в методе SaveOrCreate на основании этого свойства, определяется имя процедуры для создания новой записи или же обновления существующей.
   Добавим в проект новую страницу, с названием Image.aspx, о которой мы говорили ранее. Разметка страницы:
<%@ Page Language="C#" CodeBehind="Image.aspx.cs" Inherits="MyAvatar.Image" %>
* This source code was highlighted with Source Code Highlighter.
Код страницы:
using System;
namespace MyAvatar
{
   public partial class Image : System.Web.UI.Page
   {
       protected override void OnLoad(EventArgs e)
       {
           int userId;
           if (Request["id"] != null && int.TryParse(Request["id"], out userId))
           {
              Avatar avatar =  AvatarRepository.GetById(userId);
               if (avatar.Img != null)
               {
                   Response.Clear();
                   Response.ContentType = "image/jpeg";
                   Response.BinaryWrite(avatar.Img);
                   Response.End();
               }
           }
       }
   }
}
   Здесь все довольно просто, мы получаем из базы данных, по идентификатору, экземпляр класса аватар, проверяем задано ли у него свойство Img и отображаем его.
   Далее добавим в проект новый контрол с именем MyAvatar.ascx, который будет исполнять роль View. Разметка:
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="MyAvatar.ascx.cs" Inherits="MyAvatar.MyAvatar" %>
<link href="AvatarStyle.css" rel="stylesheet" type="text/css" />
<div class="avatarDiv">
<asp:Image runat="server" ID="avatarImage" />
<br />
<asp:Label runat="server" ID="avatarText" />
</div>
* This source code was highlighted with Source Code Highlighter.
Код:
using System;
namespace MyAvatar
{
   public partial class MyAvatar : System.Web.UI.UserControl
   {
       public int UserId;
       protected override void OnInit(EventArgs e)
       {
           Load += MyAvatarLoad;
           base.OnInit(e);
       }
       void MyAvatarLoad(object sender, EventArgs e)
       {
           if (!IsPostBack)
           {
               Avatar avatar = AvatarRepository.GetById(UserId);
               avatarImage.ImageUrl = avatar.ImageUrl;
               avatarText.Text = avatar.Text;
           }
       }
   }
}
   В данном случае мы открываем свойство UserId, для того чтобы его можно было использовать в разметке, после чего восстанавливаем из базы значения и задаем советующие свойства.
   Так же добавим к проекту файл AvatarStyle.css :
.avatarDiv
{
   border: 1px Solid Black;
   width:90px;
}
   Добавим к проекту новую страницу с названием Edit.aspx, которая будет отвечать за редактирование записи. Разметка:
<%@ Page Language="C#" CodeBehind="Edit.aspx.cs" Inherits="MyAvatar.Edit" %>
<title></title>
<form id="form1" runat="server">
  <div>
    <asp:image runat="server" id="Photo"></asp:image>
    <asp:fileupload runat="server" id="FUploader"></asp:fileupload>
    <asp:textbox runat="server" id="Text"></asp:textbox>
    <asp:button runat="server" id="SubmitBtn" text="Save"></asp:button>  
  </div>
</form>
* This source code was highlighted with Source Code Highlighter.
   Код страницы:
using System;
namespace MyAvatar
{
   public partial class Edit : System.Web.UI.Page
   {
       private Avatar _avatar;
       protected override void OnInit(EventArgs e)
       {
           Load += EditLoad;
           SubmitBtn.Click += SubmitBtnClick;
           base.OnInit(e);
       }
       void EditLoad(object sender, EventArgs e)
       {
           int userId;
           if (int.TryParse(Request["id"], out userId))
           {
               _avatar = AvatarRepository.GetById(userId);
               if (!Page.IsPostBack) Text.Text = _avatar.Text;
               Photo.ImageUrl = _avatar.ImageUrl;
           }
       }
       protected void SubmitBtnClick(object sender, EventArgs e)
       {
           if (_avatar != null)
           {
               if (FUploader.HasFile)
               {
                   _avatar.Img = FUploader.FileBytes;
               }
               _avatar.Text = Text.Text;
               AvatarRepository.SaveOrCreate(_avatar);
               Photo.ImageUrl = _avatar.ImageUrl;
           }
       }
   }
}
   И так же добавим новую страницу Index.aspx, и установим ее как страницу по умолчанию. Разметка страницы:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MyAvatar.Index" %>
<%@ Register TagName="Avatar" TagPrefix="My" Src="~/MyAvatar.ascx" %>
  <form id="form1" runat="server">
  <div>
  <my:avatar runat="server" userid="0"></my:avatar>
  <my:avatar runat="server" userid="1"></my:avatar>
  <my:avatar runat="server" userid="2"></my:avatar>
  </div>
  </form>
* This source code was highlighted with Source Code Highlighter.
   Как видно из разметки, мы определили три экземпляра нашего контрола с различными UserId, для функционального тестирования, в реальном же приложении UserId у нас будет только один, у каждого пользователя свой.
   Запустим наше приложение и браузер отобразит следующую картину:
 
   Далее прейдем по сылкам Edit.aspx?id=0 и Edit.aspx?id=1 и выставим картинки
 
 
с соответствующими надписями. После чего Index.aspx отобразит:
 
   Отлично, мы создали требуемый функционал, пора переходить к самому интересному - создать модуль на его основе.
   Если у вас по какой-либо причине не получилось дойти до этого шага, то можете взять готовое приложение из исходников и изучить его работу, так же в исходниках вы можете найти все картинки используемые мной ранее.
Создание модуля.
   Далее предполагается, что у вас уже стоит установленной CMS Dot Net Nuke, так же установлено Visual Studio Starter Kit (используемый в данной статье 
Visual Studio Starter Kit).
   Создадим новый проект DotNetNuke Compiled Module, с названием AvatarSample
   В свойствах проекта выставим Assembly Name & Default Namespace значением ByMegano.AvatarSample, а так же Target framework .net 3.5 Далее весь автоматически сгенерированный код находится в пространстве имен YourCompany.Modules.AvatarSample, я воспользуюсь Resharper’om чтобы переместить его в ByMegano.AvatarSample, вы же можете ничего не делать, так как все равно в последствии мы полностью заменим весь код.
   Далее полностью удаляем каталог Documentation и так как мы не собираемся заниматься локализацией то и App_LocalResources. К тому же в нашем проекте присутствует сразу две битых ссылки на библиотеки – DotNetNuke & Microsoft.ApplicationBlocks.Data, необходимо обновить эти ссылки через Browse, требуемые библиотеки находятся в папке bin,предварительно установленной CMS DNN.
   Осмотрим файл AvatarSampleInfo.cs он представляет код сущности модуля, совсем как класс Avatar в нашем приложении, на самом деле так и есть, только в DNN сущность принято именовать Info, полностью заменим код данного файла:
namespace ByMegano.AvatarSample.Components
{
   public class AvatarSampleInfo
   {
       private string _text = "none";
       private bool _isNewEntry = true;
       public byte[] Img { get; set; }
       public int UserId { get; set; }
       public string Text
       {
           get { return _text; }
           set { _text = value; }
       }
       public bool IsNewEntry
       {
           get { return _isNewEntry; }
           set { _isNewEntry = value; }
       }
       public string ImageUrl
       {
           get
           {
               string url = @"~/DesktopModules/Avatar/noAvatar.jpg";
               if (Img != null) url = string.Format(@"~/DesktopModules/Avatar/Image.aspx?id={0}", UserId);
               return url;
           }
       }
   }
}
   Обратите внимание на свойство ImageUrl, все модули устанавливаются в папку DesktopModules, так же, позже мы определим каталог установки модуля – Avatar, исходя из этого, мы можем точно сказать, где будут находиться файлы и прописать путь.
   Далее обратим внимание на файл DataProvider.cs,  DNN теоритически может работать не только с SQL Server, но так же с любым другим источником данных, этот класс определяет методы(#region "Abstract methods"), которые будут обязаны реализовать все провайдеры. Так же он отвечает за создание соответствующего провайдера (метод CreateProvider), используя паттерн Single Tone. Полностью заменим код данного файла:
using System.Data;
namespace ByMegano.AvatarSample.Components
{
   public abstract class DataProvider
   {
       #region "Shared/Static Methods"
       private static DataProvider objProvider = null;
       static DataProvider()
       {
           CreateProvider();
       }
       private static void CreateProvider()
       {
           objProvider = (DataProvider)DotNetNuke.Framework.Reflection.CreateObject("data", "ByMegano.AvatarSample.Components", "");
       }
       public static DataProvider Instance()
       {
           return objProvider;
       }
       #endregion
       #region "Abstract methods"
       public abstract IDataReader GetById(int userId);
       public abstract void SaveOrCreate(AvatarSampleInfo avatar);
       public abstract void DeleteById(int id);
       #endregion
   }
}
   Перейдем к файлу SqlDataProvider.cs, он представляет из себя реализацию провайдера для MS SQL Server, от нас требуется реализовать абстрактные методы класса DataProvider, а так же определить спецификатор модуля, который нужен для составления полных имен в базе данных. При вызове хранимых процедур будет составлено полное имя, учитывая данный спецификатор(полное имя составляется по правилам определенным в методе GetFullyQualifiedName по умолчанию: DatabaseOwner + ObjectQualifier + ModuleQualifier + name). Полностью заменим файл следующим кодом:
using System;
using System.Data;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Framework.Providers;
using Microsoft.ApplicationBlocks.Data;
namespace ByMegano.AvatarSample.Components
{
   public class SqlDataProvider : DataProvider
   {
       #region "Private Members"
       private const string ProviderType = "data";
       private const string ModuleQualifier = "ByMegano_";
       private ProviderConfiguration _providerConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType);
       private string _connectionString;
       private string _providerPath;
       private string _objectQualifier;
       private string _databaseOwner;
       #endregion
       #region "Constructors"
       public SqlDataProvider()
       {
           Provider objProvider = (Provider)_providerConfiguration.Providers[_providerConfiguration.DefaultProvider];
           _connectionString = Config.GetConnectionString();
           if (_connectionString == "")
           {
               _connectionString = objProvider.Attributes["connectionString"];
           }
           _providerPath = objProvider.Attributes["providerPath"];
           _objectQualifier = objProvider.Attributes["objectQualifier"];
           if (_objectQualifier != "" & _objectQualifier.EndsWith("_") == false)
           {
               _objectQualifier += "_";
           }
           _databaseOwner = objProvider.Attributes["databaseOwner"];
           if (_databaseOwner != "" & _databaseOwner.EndsWith(".") == false)
           {
               _databaseOwner += ".";
           }
       }
       #endregion
       #region "Properties"
       public string ConnectionString
       {
           get { return _connectionString; }
       }
       public string ProviderPath
       {
           get { return _providerPath; }
       }
       public string ObjectQualifier
       {
           get { return _objectQualifier; }
       }
       public string DatabaseOwner
       {
           get { return _databaseOwner; }
       }
       #endregion
       #region "Private Methods"
       private string GetFullyQualifiedName(string name)
       {
           return DatabaseOwner + ObjectQualifier + ModuleQualifier + name;
       }
       private object GetNull(object Field)
       {
           return DotNetNuke.Common.Utilities.Null.GetNull(Field, DBNull.Value);
       }
       #endregion
       #region "Public Methods"
       public override IDataReader GetById(int userId)
       {
           return (IDataReader)SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetMyAvatarByUserId"), userId);
       }
       public override void SaveOrCreate(AvatarSampleInfo avatar)
       {
           SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName(avatar.IsNewEntry ? "CreateMyAvatar" : "UpdateMyAvatar"), avatar.UserId, avatar.Text, avatar.Img);
       }
       public override void DeleteById(int id)
       {
           SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("DeleteMyAvatar"), id);
       }
       #endregion
   }
}
   Перейдем к последнему файлу в этой папке - AvatarSampleController.cs, который, является репозиторием, работающим с абстрактным классом DataProvider. Вот его реализация:
using System;
using System.Data;
namespace ByMegano.AvatarSample.Components
{
   public class AvatarController
   {
       #region "Public Methods"
       public AvatarSampleInfo GetAvatar(int userId)
       {
           AvatarSampleInfo result = new AvatarSampleInfo();
           IDataReader dr = DataProvider.Instance().GetById(userId);
           if (dr.Read())
           {
               result.IsNewEntry = false;
               result.UserId = (int)dr["UserId"];
               result.Text = dr["Text"].ToString();
               if (dr["Image"].GetType() != typeof(DBNull)) result.Img = (byte[])dr["Image"];
           }
           return result;
       }
       public void SaveOrCreate(AvatarSampleInfo objAvatar)
       {
           DataProvider.Instance().SaveOrCreate(objAvatar);
       }
       public void DeleteAvatar(int userId)
       {
           DataProvider.Instance().DeleteById(userId);
       }
       #endregion
   }
}
   Теперь перейдем к файлам корневого каталога, добавим в проект файл noAvatar.jpg и файл и AvatarStyle.css, далее мы должны переименовать файл AvatarStyle.css в module.css в этом случае, при установке, система подхватит его автоматически. Так как наш модуль не требует дополнительных настроек, удалим файл Settings.ascx.
   Добавим в проект новую страницу с именем Image.aspx, разметка:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Image.aspx.cs" Inherits="ByMegano.AvatarSample.Image" %>
* This source code was highlighted with Source Code Highlighter.
Код:
using System;
using ByMegano.AvatarSample.Components;
namespace ByMegano.AvatarSample
{
   public partial class Image : System.Web.UI.Page
   {
       protected override void OnLoad(EventArgs e)
       {
           AvatarController controller = new AvatarController();
           int userId;
           if (Request["id"] != null && int.TryParse(Request["id"], out userId))
           {
               AvatarSampleInfo avatar = controller.GetAvatar(userId);
               if (avatar.Img != null)
               {
                   Response.Clear();
                   Response.ContentType = "image/jpeg";
                   Response.BinaryWrite(avatar.Img);
                   Response.End();
               }
           }
       }
   }
}
   Как видите, мы работаем с контроллером.
   Полностью заменим разметку файла ViewAvatarSample.ascx на:
<%@ Control Language="C#" Inherits="ByMegano.AvatarSample.ViewAvatarSample" AutoEventWireup="true"   CodeBehind="ViewAvatarSample.ascx.cs" %>
<div class="avatarDiv">
  <asp:image runat="server" id="avatarImage"></asp:image>
  <asp:label runat="server" id="avatarText"></asp:label>
</div>
<asp:hyperlink id="EditLink" navigateurl='<%# EditUrl("UserId",this.UserId.ToString()) %>' runat="server">
  Edit
  <asp:image runat="server" imageurl="~/images/edit.gif" alternatetext="Edit">
</asp:image>
</asp:hyperlink>
* This source code was highlighted with Source Code Highlighter.
   Здесь стоит обратить внимание на две вещи -  Мы воспользовались методом DNN EditUrl чтобы получить ссылку на страницу редактирования модуля. И отобразили рисунок из папки images, которая так же принадлежит DNN.
   Код:
using System;
using ByMegano.AvatarSample.Components;
using DotNetNuke.Entities.Modules;
namespace ByMegano.AvatarSample
{
   partial class ViewAvatarSample : PortalModuleBase
   {
       protected override void OnInit(EventArgs e)
       {
           Load += MyAvatarLoad;
           base.OnInit(e);
       }
       protected override void OnPreRender(EventArgs e)
       {
           if (!IsPostBack) EditLink.DataBind();
           base.OnPreRender(e);
       }
       void MyAvatarLoad(object sender, EventArgs e)
       {
           if (!IsPostBack)
           {
               AvatarController controller = new AvatarController();
               AvatarSampleInfo avatar = controller.GetAvatar(this.UserId);
               avatarImage.ImageUrl = avatar.ImageUrl;
               avatarText.Text = avatar.Text;
           }
       }
   }
}
   Теперь  файл EditAvatarSample.ascx, разметка:
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="EditAvatarSample.ascx.cs" Inherits="ByMegano.AvatarSample.EditAvatarSample" %>
<div>
  <asp:image runat="server" id="Photo"></asp:image>
  <asp:fileupload runat="server" id="FUploader"></asp:fileupload>
  <asp:textbox runat="server" id="Text"></asp:textbox>
  <asp:button runat="server" id="SubmitBtn" text="Save"></asp:button>
</div>
* This source code was highlighted with Source Code Highlighter.
   Код:
using System;
using ByMegano.AvatarSample.Components;
using DotNetNuke.Common;
using DotNetNuke.Entities.Modules;
namespace ByMegano.AvatarSample
{
   partial class EditAvatarSample : PortalModuleBase
   {
       private AvatarSampleInfo _avatar;
       private AvatarController _controller;
       protected override void OnInit(EventArgs e)
       {
           _controller = new AvatarController();
           Load += EditLoad;
           SubmitBtn.Click += SubmitBtnClick;
           base.OnInit(e);
       }
       void EditLoad(object sender, EventArgs e)
       {
           if ((Request.QueryString["UserId"] != null))
           {
               int userId;
               int.TryParse(Request.QueryString["UserId"], out userId);
               _avatar = _controller.GetAvatar(userId);
               if (_avatar.UserId == 0) _avatar.UserId = userId;
               if (!Page.IsPostBack) Text.Text = _avatar.Text;
               Photo.ImageUrl = _avatar.ImageUrl;
           }
       }
       protected void SubmitBtnClick(object sender, EventArgs e)
       {
           if (_avatar != null)
           {
               if (FUploader.HasFile)
               {
                   _avatar.Img = FUploader.FileBytes;
               }
               _avatar.Text = Text.Text;
               _controller.SaveOrCreate(_avatar);
               Response.Redirect(Globals.NavigateURL(), true);
           }
       }
   }
}
   Здесь стоит отметить метод Response.Redirect(Globals.NavigateURL(), true), вызов которого перенаправит вас на страницу, с исходным View(где вы нажали ссылку Edite).
   Файл 01.00.00.SqlDataProvider, будет выполнен в момент установки модуля, туда следует поместить SQL код для базы данных:
GO
CREATE TABLE {databaseOwner}[{objectQualifier}ByMegano_MyAvatarTable](
	[UserId] [int] NOT NULL,
	[Text] [nvarchar](50) NOT NULL,
	[Image] [varbinary](max) NULL,
CONSTRAINT [PK_MyAvatarTable_1] PRIMARY KEY CLUSTERED
(
	[UserId] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_CreateMyAvatar]
@UserId int,
@Text nvarchar(50),
@Image varbinary(max) = NULL
AS
BEGIN
INSERT ByMegano_MyAvatarTable (UserId,Text,Image)
VALUES (@UserId,@Text,@Image)
END
GO
CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_DeleteMyAvatar]
@UserId int
AS
BEGIN
DELETE ByMegano_MyAvatarTable
WHERE UserId = @UserId
END
GO
CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_GetMyAvatarByUserId]
@UserId int
AS
BEGIN
SELECT * FROM ByMegano_MyAvatarTable
WHERE UserId = @UserId
END
GO
CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_UpdateMyAvatar]
@UserId int,
@Text nvarchar(50),
@Image varbinary(max) = NULL
AS
BEGIN
UPDATE ByMegano_MyAvatarTable
SET Text = @Text, Image =  @Image
WHERE UserId = @UserId
END
GO
   Следует обратить внимание на то, что непосредственно перед исполнением DNN заменит метки {databaseOwner}{objectQualifier} соответствующими значениями.
   По аналогии файл Uninstall.SqlDataProvider выполнит SQL код в момент удаления модуля:
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_CreateMyAvatar]') AND type in (N'P', N'PC'))
DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_CreateMyAvatar]
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_DeleteMyAvatar]') AND type in (N'P', N'PC'))
DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_DeleteMyAvatar]
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_GetMyAvatarByUserId]') AND type in (N'P', N'PC'))
DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_GetMyAvatarByUserId]
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_UpdateMyAvatar]') AND type in (N'P', N'PC'))
DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_UpdateMyAvatar]
GO
IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_MyAvatarTable]') AND type in (N'U'))
DROP TABLE {databaseOwner}[{objectQualifier}ByMegano_MyAvatarTable]
GO
   Последний файл - AvatarSample.dnn, это метафайл модуля, в нем описываются все файлы, из которых он состоит:
 
   
     Avatar
     Avatar
     Avatar
     Avatar
     A Avatar module
     01.00.00
ByMegano.AvatarSample.Components.AvatarController
     
       
         Avatar
         0
         
           
             DesktopModules/Avatar/ViewAvatarSample.ascx
             View
             
           
           
             Edit
             Edit Content
             DesktopModules/Avatar/EditAvatarSample.ascx
             Edit
             
           
         
       
     
     
       
         ByMegano.AvatarSample.dll
       
       
         ViewAvatarSample.ascx
       
       
         EditAvatarSample.ascx
       
       
         01.00.00.SqlDataProvider
       
       
         Uninstall.SqlDataProvider
       
       
         noAvatar.jpg
       
       
         Image.aspx
       
       
         module.css
       
     
   
 
   Как видите файлов всего 8, это файлы разметки, css, а так же библиотека сборки. Вы можете произвести Build проекта, собрать указанные файлы в zip архив и установить в DNN. Так же готовый к установке модуль можно найти в исходниках.
   
Заключение:
   В данной статье мы рассмотрели создание простого модуля. Однако, из за большого количества материала, не было затронуто множество других тем, таких как, локализация, настройка модуля и прочее, но базовые знания, полученные здесь,  помогут вам в дальнейшем сосредоточиться на достижении поставленной задачи, не теряясь в деталях.
   Если вы чего-то не до поняли, или что то вышло не так, то всегда можете скачать файлы проектов и модуля прикрепленные к статье и сравнить их между собой, а так же, создать проект нового модуля и посмотреть его код.
Александр Кобелев aka Megano