вторник, 8 марта 2011 г.

Модули DNN

Исходники к статье

Версия 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 который будет представлять сущность аватара, вот его код:

  1. namespace MyAvatar  
  2.   
  3. {  
  4.   
  5.    internal class Avatar  
  6.   
  7.    {         
  8.   
  9.        public int UserId { getset; }  
  10.   
  11.        public byte[] Img{ getset; }  
  12.   
  13.        private string _text = "none";  
  14.   
  15.        public string Text  
  16.   
  17.        {  
  18.   
  19.            get { return _text; }  
  20.   
  21.            set { _text = value; }  
  22.   
  23.        }  
  24.   
  25.        private bool _isNewEntry = true;  
  26.   
  27.        public bool IsNewEntry  
  28.   
  29.        {  
  30.   
  31.            get { return _isNewEntry; }  
  32.   
  33.            set { _isNewEntry = value; }  
  34.   
  35.        }  
  36.   
  37.        public string ImageUrl  
  38.   
  39.        {  
  40.   
  41.            get  
  42.   
  43.            {  
  44.   
  45.                string url =  @"~/noAvatar.jpg";  
  46.   
  47.                if (Img != null) url = string.Format(@"~/Image.aspx?id={0}", UserId);  
  48.   
  49.                return url;  
  50.   
  51.            }  
  52.   
  53.        }  
  54.   
  55.    }  
  56.   
  57. }  


Свойства: UserId – уникально идентифицирует каждого пользователя, Img – картинка аватара для пользователя, Text – подпись под аватаром, IsNewEntry – определяет новая ли это запись или данные восстановлены из базы данных, ImageUrl – предоставляет ссылку на картинку аватара.

Обратим внимание на свойство ImageUrl, в нем определен файл noAvatar.jpg, который нужно добавить к проекту в корневой каталог (взять можно из исходников), а так же указана страница Image.aspx. В данном случае, в академических целях, проще хранить картинки в базе, чем работать с файловой системой, данная страница предоставляет картинки из базы по id.

Создайте базу данных с именем MyAvatarDB. Из предыдущего листинга видно, что для хранения состояния модуля достаточно трех полей UserId, Text, Img, остальные являются вычисляемыми. Далее создадим таблицу и CRUD процедуры, листинг:

  1. USE MyAvatarDB  
  2.   
  3. GO  
  4.   
  5. CREATE TABLE MyAvatarTable(  
  6.   
  7. UserId int NOT NULL PRIMARY KEY,  
  8.   
  9. Text nvarchar(50) NOT NULL,  
  10.   
  11. Image varbinary(maxNULL)  
  12.   
  13. GO  
  14.   
  15.   
  16.   
  17. CREATE PROCEDURE CreateMyAvatar  
  18.   
  19. @UserId int,  
  20.   
  21. @Text nvarchar(50),  
  22.   
  23. @Image varbinary(max) = NULL  
  24.   
  25. AS  
  26.   
  27. BEGIN  
  28.   
  29. INSERT MyAvatarTable (UserId,Text,Image)  
  30.   
  31. VALUES (@UserId,@Text,@Image)  
  32.   
  33. END  
  34.   
  35. GO  
  36.   
  37.   
  38.   
  39. CREATE PROCEDURE DeleteMyAvatar  
  40.   
  41. @UserId int  
  42.   
  43. AS  
  44.   
  45. BEGIN  
  46.   
  47. DELETE MyAvatarTable  
  48.   
  49. WHERE UserId = @UserId  
  50.   
  51. END  
  52.   
  53. GO  
  54.   
  55.   
  56.   
  57. CREATE PROCEDURE GetMyAvatarByUserId  
  58.   
  59. @UserId int  
  60.   
  61. AS  
  62.   
  63. BEGIN  
  64.   
  65. SELECT * FROM MyAvatarTable  
  66.   
  67. WHERE UserId = @UserId  
  68.   
  69. END  
  70.   
  71. GO  
  72.   
  73.   
  74.   
  75. CREATE PROCEDURE UpdateMyAvatar  
  76.   
  77. @UserId int,  
  78.   
  79. @Text nvarchar(50),  
  80.   
  81. @Image varbinary(max) = NULL  
  82.   
  83. AS  
  84.   
  85. BEGIN  
  86.   
  87. UPDATE MyAvatarTable  
  88.   
  89. SET Text = @Text, Image =  @Image  
  90.   
  91. WHERE UserId = @UserId  
  92.   
  93. END  
  94.   
  95. 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. код:

  1. using System;  
  2.   
  3. using System.Configuration;  
  4.   
  5. using System.Data;  
  6.   
  7. using System.Data.SqlClient;  
  8.   
  9. namespace MyAvatar  
  10.   
  11. {  
  12.   
  13.    internal static class AvatarRepository  
  14.   
  15.    {  
  16.   
  17.        private static SqlConnection BuildConnection()  
  18.   
  19.        {  
  20.   
  21.            return new SqlConnection(ConfigurationManager.ConnectionStrings["MyAvatarConString"].ConnectionString);  
  22.   
  23.        }  
  24.   
  25.        public static Avatar GetById(int userId)  
  26.   
  27.        {  
  28.   
  29.            Avatar avatar = new Avatar { UserId = userId };  
  30.   
  31.            using (SqlConnection conn = BuildConnection())  
  32.   
  33.            {  
  34.   
  35.                SqlCommand command = conn.CreateCommand();  
  36.   
  37.   
  38.   
  39.                command.CommandText = "GetMyAvatarByUserId";  
  40.   
  41.                command.CommandType = CommandType.StoredProcedure;  
  42.   
  43.                command.Parameters.AddWithValue("UserId", userId);  
  44.   
  45.   
  46.   
  47.                conn.Open();  
  48.   
  49.                var result = command.ExecuteReader();  
  50.   
  51.                result.Read();  
  52.   
  53.   
  54.   
  55.                if (result.HasRows)  
  56.   
  57.                {  
  58.   
  59.                    avatar.IsNewEntry = false;  
  60.   
  61.                    avatar.Text = result["Text"].ToString();  
  62.   
  63.                    if (result["Image"].GetType() != typeof(DBNull))  
  64.   
  65.                        avatar.Img = (byte[])result["Image"];  
  66.   
  67.                    conn.Close();  
  68.   
  69.                }  
  70.   
  71.            }  
  72.   
  73.            return avatar;  
  74.   
  75.        }  
  76.   
  77.        public static void SaveOrCreate(Avatar avatar)  
  78.   
  79.        {  
  80.   
  81.            using (SqlConnection conn = BuildConnection())  
  82.   
  83.            {   
  84.   
  85.                SqlCommand command = conn.CreateCommand();  
  86.   
  87.                command.CommandText = (avatar.IsNewEntry ? "CreateMyAvatar" : "UpdateMyAvatar");  
  88.   
  89.                command.CommandType = CommandType.StoredProcedure;  
  90.   
  91.   
  92.   
  93.                command.Parameters.AddWithValue("UserId", avatar.UserId);  
  94.   
  95.                command.Parameters.AddWithValue("Text", avatar.Text);  
  96.   
  97.                command.Parameters.AddWithValue("Image", avatar.Img);  
  98.   
  99.                conn.Open();  
  100.   
  101.                command.ExecuteNonQuery();  
  102.   
  103.                conn.Close();  
  104.   
  105.            }  
  106.   
  107.        }  
  108.   
  109.        public static void DeleteById(int id)  
  110.   
  111.        {  
  112.   
  113.            using (SqlConnection conn = BuildConnection())  
  114.   
  115.            {  
  116.   
  117.                SqlCommand command = conn.CreateCommand();  
  118.   
  119.                command.CommandText = "DeleteMyAvatar";  
  120.   
  121.                command.CommandType = CommandType.StoredProcedure;  
  122.   
  123.   
  124.   
  125.                command.Parameters.AddWithValue("UserId", id);  
  126.   
  127.                conn.Open();  
  128.   
  129.                command.ExecuteNonQuery();  
  130.   
  131.                conn.Close();  
  132.   
  133.            }  
  134.   
  135.        }  
  136.   
  137.    }  
  138.   
  139. }  


Единственное на что следует обратить внимание – при вызове метода 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.


Код страницы:

  1. using System;  
  2.   
  3. namespace MyAvatar  
  4.   
  5. {  
  6.   
  7.    public partial class Image : System.Web.UI.Page  
  8.   
  9.    {  
  10.   
  11.        protected override void OnLoad(EventArgs e)  
  12.   
  13.        {  
  14.   
  15.            int userId;  
  16.   
  17.            if (Request["id"] != null && int.TryParse(Request["id"], out userId))  
  18.   
  19.            {  
  20.   
  21.               Avatar avatar =  AvatarRepository.GetById(userId);  
  22.   
  23.                if (avatar.Img != null)  
  24.   
  25.                {  
  26.   
  27.                    Response.Clear();  
  28.   
  29.                    Response.ContentType = "image/jpeg";  
  30.   
  31.                    Response.BinaryWrite(avatar.Img);  
  32.   
  33.                    Response.End();  
  34.   
  35.                }  
  36.   
  37.            }  
  38.   
  39.        }  
  40.   
  41.    }  
  42.   
  43. }  


Здесь все довольно просто, мы получаем из базы данных, по идентификатору, экземпляр класса аватар, проверяем задано ли у него свойство 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.


Код:

  1. using System;  
  2.   
  3. namespace MyAvatar  
  4.   
  5. {  
  6.   
  7.    public partial class MyAvatar : System.Web.UI.UserControl  
  8.   
  9.    {  
  10.   
  11.        public int UserId;  
  12.   
  13.        protected override void OnInit(EventArgs e)  
  14.   
  15.        {  
  16.   
  17.            Load += MyAvatarLoad;  
  18.   
  19.            base.OnInit(e);  
  20.   
  21.        }  
  22.   
  23.        void MyAvatarLoad(object sender, EventArgs e)  
  24.   
  25.        {  
  26.   
  27.            if (!IsPostBack)  
  28.   
  29.            {  
  30.   
  31.                Avatar avatar = AvatarRepository.GetById(UserId);  
  32.   
  33.                avatarImage.ImageUrl = avatar.ImageUrl;  
  34.   
  35.                avatarText.Text = avatar.Text;  
  36.   
  37.            }  
  38.   
  39.        }  
  40.   
  41.    }  
  42.   
  43. }  


В данном случае мы открываем свойство UserId, для того чтобы его можно было использовать в разметке, после чего восстанавливаем из базы значения и задаем советующие свойства.

Так же добавим к проекту файл AvatarStyle.css :

  1. .avatarDiv  
  2.   
  3. {  
  4.   
  5.    border1px Solid Black;  
  6.   
  7.    width:90px;  
  8.   
  9. }  


Добавим к проекту новую страницу с названием 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.


Код страницы:

  1. using System;  
  2.   
  3. namespace MyAvatar  
  4.   
  5. {  
  6.   
  7.    public partial class Edit : System.Web.UI.Page  
  8.   
  9.    {  
  10.   
  11.        private Avatar _avatar;  
  12.   
  13.        protected override void OnInit(EventArgs e)  
  14.   
  15.        {  
  16.   
  17.            Load += EditLoad;  
  18.   
  19.            SubmitBtn.Click += SubmitBtnClick;  
  20.   
  21.            base.OnInit(e);  
  22.   
  23.        }  
  24.   
  25.        void EditLoad(object sender, EventArgs e)  
  26.   
  27.        {  
  28.   
  29.            int userId;  
  30.   
  31.            if (int.TryParse(Request["id"], out userId))  
  32.   
  33.            {  
  34.   
  35.                _avatar = AvatarRepository.GetById(userId);  
  36.   
  37.                if (!Page.IsPostBack) Text.Text = _avatar.Text;  
  38.   
  39.                Photo.ImageUrl = _avatar.ImageUrl;  
  40.   
  41.            }  
  42.   
  43.        }  
  44.   
  45.        protected void SubmitBtnClick(object sender, EventArgs e)  
  46.   
  47.        {  
  48.   
  49.            if (_avatar != null)  
  50.   
  51.            {  
  52.   
  53.                if (FUploader.HasFile)  
  54.   
  55.                {  
  56.   
  57.                    _avatar.Img = FUploader.FileBytes;  
  58.   
  59.                }  
  60.   
  61.                _avatar.Text = Text.Text;  
  62.   
  63.                AvatarRepository.SaveOrCreate(_avatar);  
  64.   
  65.                Photo.ImageUrl = _avatar.ImageUrl;  
  66.   
  67.            }  
  68.   
  69.        }  
  70.   
  71.    }  
  72.   
  73. }  


И так же добавим новую страницу 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 у нас будет только один, у каждого пользователя свой.

Запустим наше приложение и браузер отобразит следующую картину:

DNN Module first run

Далее прейдем по сылкам Edit.aspx?id=0 и Edit.aspx?id=1 и выставим картинки

DNN Module host

DNN Module admin

с соответствующими надписями. После чего Index.aspx отобразит:

DNN Module last run

Отлично, мы создали требуемый функционал, пора переходить к самому интересному - создать модуль на его основе.

Если у вас по какой-либо причине не получилось дойти до этого шага, то можете взять готовое приложение из исходников и изучить его работу, так же в исходниках вы можете найти все картинки используемые мной ранее.

Создание модуля.



Далее предполагается, что у вас уже стоит установленной 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, полностью заменим код данного файла:

  1. namespace ByMegano.AvatarSample.Components  
  2.   
  3. {  
  4.   
  5.    public class AvatarSampleInfo  
  6.   
  7.    {  
  8.   
  9.        private string _text = "none";  
  10.   
  11.        private bool _isNewEntry = true;  
  12.   
  13.        public byte[] Img { getset; }  
  14.   
  15.        public int UserId { getset; }  
  16.   
  17.        public string Text  
  18.   
  19.        {  
  20.   
  21.            get { return _text; }  
  22.   
  23.            set { _text = value; }  
  24.   
  25.        }  
  26.   
  27.        public bool IsNewEntry  
  28.   
  29.        {  
  30.   
  31.            get { return _isNewEntry; }  
  32.   
  33.            set { _isNewEntry = value; }  
  34.   
  35.        }  
  36.   
  37.        public string ImageUrl  
  38.   
  39.        {  
  40.   
  41.            get  
  42.   
  43.            {  
  44.   
  45.                string url = @"~/DesktopModules/Avatar/noAvatar.jpg";  
  46.   
  47.                if (Img != null) url = string.Format(@"~/DesktopModules/Avatar/Image.aspx?id={0}", UserId);  
  48.   
  49.                return url;  
  50.   
  51.            }  
  52.   
  53.        }  
  54.   
  55.    }  
  56.   
  57. }  


Обратите внимание на свойство ImageUrl, все модули устанавливаются в папку DesktopModules, так же, позже мы определим каталог установки модуля – Avatar, исходя из этого, мы можем точно сказать, где будут находиться файлы и прописать путь.

Далее обратим внимание на файл DataProvider.cs, DNN теоритически может работать не только с SQL Server, но так же с любым другим источником данных, этот класс определяет методы(#region "Abstract methods"), которые будут обязаны реализовать все провайдеры. Так же он отвечает за создание соответствующего провайдера (метод CreateProvider), используя паттерн Single Tone. Полностью заменим код данного файла:

  1. using System.Data;  
  2.   
  3. namespace ByMegano.AvatarSample.Components  
  4.   
  5. {  
  6.   
  7.    public abstract class DataProvider  
  8.   
  9.    {  
  10.  
  11.        #region "Shared/Static Methods"  
  12.   
  13.        private static DataProvider objProvider = null;  
  14.   
  15.        static DataProvider()  
  16.   
  17.        {  
  18.   
  19.            CreateProvider();  
  20.   
  21.        }  
  22.   
  23.        private static void CreateProvider()  
  24.   
  25.        {  
  26.   
  27.            objProvider = (DataProvider)DotNetNuke.Framework.Reflection.CreateObject("data""ByMegano.AvatarSample.Components""");  
  28.   
  29.        }  
  30.   
  31.        public static DataProvider Instance()  
  32.   
  33.        {  
  34.   
  35.            return objProvider;  
  36.   
  37.        }  
  38.  
  39.        #endregion  
  40.  
  41.        #region "Abstract methods"  
  42.   
  43.        public abstract IDataReader GetById(int userId);  
  44.   
  45.        public abstract void SaveOrCreate(AvatarSampleInfo avatar);  
  46.   
  47.        public abstract void DeleteById(int id);  
  48.  
  49.        #endregion  
  50.   
  51.    }  
  52.   
  53. }  


Перейдем к файлу SqlDataProvider.cs, он представляет из себя реализацию провайдера для MS SQL Server, от нас требуется реализовать абстрактные методы класса DataProvider, а так же определить спецификатор модуля, который нужен для составления полных имен в базе данных. При вызове хранимых процедур будет составлено полное имя, учитывая данный спецификатор(полное имя составляется по правилам определенным в методе GetFullyQualifiedName по умолчанию: DatabaseOwner + ObjectQualifier + ModuleQualifier + name). Полностью заменим файл следующим кодом:

  1. using System;  
  2.   
  3. using System.Data;  
  4.   
  5. using DotNetNuke.Common.Utilities;  
  6.   
  7. using DotNetNuke.Framework.Providers;  
  8.   
  9. using Microsoft.ApplicationBlocks.Data;  
  10.   
  11. namespace ByMegano.AvatarSample.Components  
  12.   
  13. {  
  14.   
  15.    public class SqlDataProvider : DataProvider  
  16.   
  17.    {  
  18.  
  19.        #region "Private Members"  
  20.   
  21.        private const string ProviderType = "data";  
  22.   
  23.        private const string ModuleQualifier = "ByMegano_";  
  24.   
  25.        private ProviderConfiguration _providerConfiguration = ProviderConfiguration.GetProviderConfiguration(ProviderType);  
  26.   
  27.        private string _connectionString;  
  28.   
  29.        private string _providerPath;  
  30.   
  31.        private string _objectQualifier;  
  32.   
  33.        private string _databaseOwner;  
  34.  
  35.        #endregion  
  36.  
  37.        #region "Constructors"  
  38.   
  39.        public SqlDataProvider()  
  40.   
  41.        {  
  42.   
  43.            Provider objProvider = (Provider)_providerConfiguration.Providers[_providerConfiguration.DefaultProvider];  
  44.   
  45.            _connectionString = Config.GetConnectionString();  
  46.   
  47.            if (_connectionString == "")  
  48.   
  49.            {  
  50.   
  51.                _connectionString = objProvider.Attributes["connectionString"];  
  52.   
  53.            }  
  54.   
  55.            _providerPath = objProvider.Attributes["providerPath"];  
  56.   
  57.            _objectQualifier = objProvider.Attributes["objectQualifier"];  
  58.   
  59.            if (_objectQualifier != "" & _objectQualifier.EndsWith("_") == false)  
  60.   
  61.            {  
  62.   
  63.                _objectQualifier += "_";  
  64.   
  65.            }  
  66.   
  67.            _databaseOwner = objProvider.Attributes["databaseOwner"];  
  68.   
  69.            if (_databaseOwner != "" & _databaseOwner.EndsWith(".") == false)  
  70.   
  71.            {  
  72.   
  73.                _databaseOwner += ".";  
  74.   
  75.            }  
  76.   
  77.        }  
  78.  
  79.        #endregion  
  80.  
  81.        #region "Properties"  
  82.   
  83.        public string ConnectionString  
  84.   
  85.        {  
  86.   
  87.            get { return _connectionString; }  
  88.   
  89.        }  
  90.   
  91.        public string ProviderPath  
  92.   
  93.        {  
  94.   
  95.            get { return _providerPath; }  
  96.   
  97.        }  
  98.   
  99.        public string ObjectQualifier  
  100.   
  101.        {  
  102.   
  103.            get { return _objectQualifier; }  
  104.   
  105.        }  
  106.   
  107.        public string DatabaseOwner  
  108.   
  109.        {  
  110.   
  111.            get { return _databaseOwner; }  
  112.   
  113.        }  
  114.  
  115.        #endregion  
  116.  
  117.        #region "Private Methods"  
  118.   
  119.        private string GetFullyQualifiedName(string name)  
  120.   
  121.        {  
  122.   
  123.            return DatabaseOwner + ObjectQualifier + ModuleQualifier + name;  
  124.   
  125.        }  
  126.   
  127.        private object GetNull(object Field)  
  128.   
  129.        {  
  130.   
  131.            return DotNetNuke.Common.Utilities.Null.GetNull(Field, DBNull.Value);  
  132.   
  133.        }  
  134.  
  135.        #endregion  
  136.  
  137.        #region "Public Methods"  
  138.   
  139.        public override IDataReader GetById(int userId)  
  140.   
  141.        {  
  142.   
  143.            return (IDataReader)SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetMyAvatarByUserId"), userId);  
  144.   
  145.        }  
  146.   
  147.        public override void SaveOrCreate(AvatarSampleInfo avatar)  
  148.   
  149.        {  
  150.   
  151.            SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName(avatar.IsNewEntry ? "CreateMyAvatar" : "UpdateMyAvatar"), avatar.UserId, avatar.Text, avatar.Img);  
  152.   
  153.        }  
  154.   
  155.        public override void DeleteById(int id)  
  156.   
  157.        {  
  158.   
  159.            SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("DeleteMyAvatar"), id);  
  160.   
  161.        }  
  162.  
  163.        #endregion  
  164.   
  165.    }  
  166.   
  167. }  


Перейдем к последнему файлу в этой папке - AvatarSampleController.cs, который, является репозиторием, работающим с абстрактным классом DataProvider. Вот его реализация:

  1. using System;  
  2.   
  3. using System.Data;  
  4.   
  5. namespace ByMegano.AvatarSample.Components  
  6.   
  7. {  
  8.   
  9.    public class AvatarController  
  10.   
  11.    {  
  12.  
  13.        #region "Public Methods"  
  14.   
  15.        public AvatarSampleInfo GetAvatar(int userId)  
  16.   
  17.        {  
  18.   
  19.            AvatarSampleInfo result = new AvatarSampleInfo();  
  20.   
  21.   
  22.   
  23.            IDataReader dr = DataProvider.Instance().GetById(userId);  
  24.   
  25.            if (dr.Read())  
  26.   
  27.            {  
  28.   
  29.                result.IsNewEntry = false;  
  30.   
  31.                result.UserId = (int)dr["UserId"];  
  32.   
  33.                result.Text = dr["Text"].ToString();  
  34.   
  35.                if (dr["Image"].GetType() != typeof(DBNull)) result.Img = (byte[])dr["Image"];  
  36.   
  37.            }  
  38.   
  39.            return result;  
  40.   
  41.        }  
  42.   
  43.        public void SaveOrCreate(AvatarSampleInfo objAvatar)  
  44.   
  45.        {  
  46.   
  47.            DataProvider.Instance().SaveOrCreate(objAvatar);  
  48.   
  49.        }  
  50.   
  51.        public void DeleteAvatar(int userId)  
  52.   
  53.        {  
  54.   
  55.            DataProvider.Instance().DeleteById(userId);  
  56.   
  57.        }  
  58.  
  59.        #endregion  
  60.   
  61.    }  
  62.   
  63. }  


Теперь перейдем к файлам корневого каталога, добавим в проект файл 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.


Код:

  1. using System;  
  2.   
  3. using ByMegano.AvatarSample.Components;  
  4.   
  5. namespace ByMegano.AvatarSample  
  6.   
  7. {  
  8.   
  9.    public partial class Image : System.Web.UI.Page  
  10.   
  11.    {  
  12.   
  13.        protected override void OnLoad(EventArgs e)  
  14.   
  15.        {  
  16.   
  17.            AvatarController controller = new AvatarController();  
  18.   
  19.            int userId;  
  20.   
  21.            if (Request["id"] != null && int.TryParse(Request["id"], out userId))  
  22.   
  23.            {  
  24.   
  25.                AvatarSampleInfo avatar = controller.GetAvatar(userId);  
  26.   
  27.                if (avatar.Img != null)  
  28.   
  29.                {  
  30.   
  31.                    Response.Clear();  
  32.   
  33.                    Response.ContentType = "image/jpeg";  
  34.   
  35.                    Response.BinaryWrite(avatar.Img);  
  36.   
  37.                    Response.End();  
  38.   
  39.                }  
  40.   
  41.            }  
  42.   
  43.        }  
  44.   
  45.    }  
  46.   
  47. }  


Как видите, мы работаем с контроллером.

Полностью заменим разметку файла 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.

Код:

  1. using System;  
  2.   
  3. using ByMegano.AvatarSample.Components;  
  4.   
  5. using DotNetNuke.Entities.Modules;  
  6.   
  7. namespace ByMegano.AvatarSample  
  8.   
  9. {  
  10.   
  11.    partial class ViewAvatarSample : PortalModuleBase  
  12.   
  13.    {  
  14.   
  15.        protected override void OnInit(EventArgs e)  
  16.   
  17.        {  
  18.   
  19.            Load += MyAvatarLoad;  
  20.   
  21.            base.OnInit(e);  
  22.   
  23.        }  
  24.   
  25.        protected override void OnPreRender(EventArgs e)  
  26.   
  27.        {  
  28.   
  29.            if (!IsPostBack) EditLink.DataBind();  
  30.   
  31.            base.OnPreRender(e);  
  32.   
  33.        }  
  34.   
  35.        void MyAvatarLoad(object sender, EventArgs e)  
  36.   
  37.        {  
  38.   
  39.            if (!IsPostBack)  
  40.   
  41.            {  
  42.   
  43.                AvatarController controller = new AvatarController();  
  44.   
  45.                AvatarSampleInfo avatar = controller.GetAvatar(this.UserId);  
  46.   
  47.                avatarImage.ImageUrl = avatar.ImageUrl;  
  48.   
  49.                avatarText.Text = avatar.Text;  
  50.   
  51.            }  
  52.   
  53.        }  
  54.   
  55.    }  
  56.   
  57. }  


Теперь файл 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.


Код:

  1. using System;  
  2.   
  3. using ByMegano.AvatarSample.Components;  
  4.   
  5. using DotNetNuke.Common;  
  6.   
  7. using DotNetNuke.Entities.Modules;  
  8.   
  9. namespace ByMegano.AvatarSample  
  10.   
  11. {  
  12.   
  13.    partial class EditAvatarSample : PortalModuleBase  
  14.   
  15.    {  
  16.   
  17.        private AvatarSampleInfo _avatar;  
  18.   
  19.        private AvatarController _controller;  
  20.   
  21.        protected override void OnInit(EventArgs e)  
  22.   
  23.        {  
  24.   
  25.            _controller = new AvatarController();  
  26.   
  27.            Load += EditLoad;  
  28.   
  29.            SubmitBtn.Click += SubmitBtnClick;  
  30.   
  31.            base.OnInit(e);  
  32.   
  33.        }  
  34.   
  35.        void EditLoad(object sender, EventArgs e)  
  36.   
  37.        {  
  38.   
  39.            if ((Request.QueryString["UserId"] != null))  
  40.   
  41.            {  
  42.   
  43.                int userId;  
  44.   
  45.                int.TryParse(Request.QueryString["UserId"], out userId);  
  46.   
  47.   
  48.   
  49.                _avatar = _controller.GetAvatar(userId);  
  50.   
  51.                if (_avatar.UserId == 0) _avatar.UserId = userId;  
  52.   
  53.                if (!Page.IsPostBack) Text.Text = _avatar.Text;  
  54.   
  55.                Photo.ImageUrl = _avatar.ImageUrl;  
  56.   
  57.            }  
  58.   
  59.        }  
  60.   
  61.        protected void SubmitBtnClick(object sender, EventArgs e)  
  62.   
  63.        {  
  64.   
  65.            if (_avatar != null)  
  66.   
  67.            {  
  68.   
  69.                if (FUploader.HasFile)  
  70.   
  71.                {  
  72.   
  73.                    _avatar.Img = FUploader.FileBytes;  
  74.   
  75.                }  
  76.   
  77.                _avatar.Text = Text.Text;  
  78.   
  79.                _controller.SaveOrCreate(_avatar);  
  80.   
  81.                Response.Redirect(Globals.NavigateURL(), true);  
  82.   
  83.            }  
  84.   
  85.        }  
  86.   
  87.    }  
  88.   
  89. }  


Здесь стоит отметить метод Response.Redirect(Globals.NavigateURL(), true), вызов которого перенаправит вас на страницу, с исходным View(где вы нажали ссылку Edite).

Файл 01.00.00.SqlDataProvider, будет выполнен в момент установки модуля, туда следует поместить SQL код для базы данных:

  1. GO  
  2.   
  3. CREATE TABLE {databaseOwner}[{objectQualifier}ByMegano_MyAvatarTable](  
  4.   
  5.     [UserId] [intNOT NULL,  
  6.   
  7.     [Text] [nvarchar](50) NOT NULL,  
  8.   
  9.     [Image] [varbinary](maxNULL,  
  10.   
  11. CONSTRAINT [PK_MyAvatarTable_1] PRIMARY KEY CLUSTERED  
  12.   
  13. (  
  14.   
  15.     [UserId] ASC  
  16.   
  17. )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ONON [PRIMARY]  
  18.   
  19. ON [PRIMARY]  
  20.   
  21. GO  
  22.   
  23.   
  24.   
  25. CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_CreateMyAvatar]  
  26.   
  27. @UserId int,  
  28.   
  29. @Text nvarchar(50),  
  30.   
  31. @Image varbinary(max) = NULL  
  32.   
  33. AS  
  34.   
  35. BEGIN  
  36.   
  37. INSERT ByMegano_MyAvatarTable (UserId,Text,Image)  
  38.   
  39. VALUES (@UserId,@Text,@Image)  
  40.   
  41. END  
  42.   
  43. GO  
  44.   
  45.   
  46.   
  47. CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_DeleteMyAvatar]  
  48.   
  49. @UserId int  
  50.   
  51. AS  
  52.   
  53. BEGIN  
  54.   
  55. DELETE ByMegano_MyAvatarTable  
  56.   
  57. WHERE UserId = @UserId  
  58.   
  59. END  
  60.   
  61. GO  
  62.   
  63.   
  64.   
  65. CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_GetMyAvatarByUserId]  
  66.   
  67. @UserId int  
  68.   
  69. AS  
  70.   
  71. BEGIN  
  72.   
  73. SELECT * FROM ByMegano_MyAvatarTable  
  74.   
  75. WHERE UserId = @UserId  
  76.   
  77. END  
  78.   
  79. GO  
  80.   
  81.   
  82.   
  83. CREATE PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_UpdateMyAvatar]  
  84.   
  85. @UserId int,  
  86.   
  87. @Text nvarchar(50),  
  88.   
  89. @Image varbinary(max) = NULL  
  90.   
  91. AS  
  92.   
  93. BEGIN  
  94.   
  95. UPDATE ByMegano_MyAvatarTable  
  96.   
  97. SET Text = @Text, Image =  @Image  
  98.   
  99. WHERE UserId = @UserId  
  100.   
  101. END  
  102.   
  103. GO  


Следует обратить внимание на то, что непосредственно перед исполнением DNN заменит метки {databaseOwner}{objectQualifier} соответствующими значениями.

По аналогии файл Uninstall.SqlDataProvider выполнит SQL код в момент удаления модуля:

  1. GO  
  2.   
  3. IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_CreateMyAvatar]'AND type in (N'P', N'PC'))  
  4.   
  5. DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_CreateMyAvatar]  
  6.   
  7. GO  
  8.   
  9.   
  10.   
  11. IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_DeleteMyAvatar]'AND type in (N'P', N'PC'))  
  12.   
  13. DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_DeleteMyAvatar]  
  14.   
  15. GO  
  16.   
  17.   
  18.   
  19. IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_GetMyAvatarByUserId]'AND type in (N'P', N'PC'))  
  20.   
  21. DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_GetMyAvatarByUserId]  
  22.   
  23. GO  
  24.   
  25.   
  26.   
  27. IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_UpdateMyAvatar]'AND type in (N'P', N'PC'))  
  28.   
  29. DROP PROCEDURE {databaseOwner}[{objectQualifier}ByMegano_UpdateMyAvatar]  
  30.   
  31. GO  
  32.   
  33.   
  34.   
  35. IF  EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}ByMegano_MyAvatarTable]'AND type in (N'U'))  
  36.   
  37. DROP TABLE {databaseOwner}[{objectQualifier}ByMegano_MyAvatarTable]  
  38.   
  39. GO  


Последний файл - AvatarSample.dnn, это метафайл модуля, в нем описываются все файлы, из которых он состоит:

  1. <dotnetnuke version="3.0" type="Module">  
  2.   
  3.  <folders>  
  4.   
  5.    <folder>  
  6.   
  7.      <name>Avatar</name>  
  8.   
  9.      <friendlyname>Avatar</friendlyname>  
  10.   
  11.      <foldername>Avatar</foldername>  
  12.   
  13.      <modulename>Avatar</modulename>  
  14.   
  15.      <description>A Avatar module</description>  
  16.   
  17.      <version>01.00.00</version>  
  18.   
  19. <businesscontrollerclass>ByMegano.AvatarSample.Components.AvatarController</businesscontrollerclass>  
  20.   
  21.      <modules>  
  22.   
  23.        <module>  
  24.   
  25.          <friendlyname>Avatar</friendlyname>  
  26.   
  27.          <cachetime>0</cachetime>  
  28.   
  29.          <controls>  
  30.   
  31.            <control>  
  32.   
  33.              <src>DesktopModules/Avatar/ViewAvatarSample.ascx</src>  
  34.   
  35.              <type>View</type>  
  36.   
  37.              <helpurl></helpurl>  
  38.   
  39.            </control>  
  40.   
  41.            <control>  
  42.   
  43.              <key>Edit</key>  
  44.   
  45.              <title>Edit Content</title>  
  46.   
  47.              <src>DesktopModules/Avatar/EditAvatarSample.ascx</src>  
  48.   
  49.              <type>Edit</type>  
  50.   
  51.              <helpurl></helpurl>  
  52.   
  53.            </control>  
  54.   
  55.          </controls>  
  56.   
  57.        </module>  
  58.   
  59.      </modules>  
  60.   
  61.      <files>  
  62.   
  63.        <file>  
  64.   
  65.          <name>ByMegano.AvatarSample.dll</name>  
  66.   
  67.        </file>  
  68.   
  69.        <file>  
  70.   
  71.          <name>ViewAvatarSample.ascx</name>  
  72.   
  73.        </file>  
  74.   
  75.        <file>  
  76.   
  77.          <name>EditAvatarSample.ascx</name>  
  78.   
  79.        </file>  
  80.   
  81.        <file>  
  82.   
  83.          <name>01.00.00.SqlDataProvider</name>  
  84.   
  85.        </file>  
  86.   
  87.        <file>  
  88.   
  89.          <name>Uninstall.SqlDataProvider</name>  
  90.   
  91.        </file>  
  92.   
  93.        <file>  
  94.   
  95.          <name>noAvatar.jpg</name>  
  96.   
  97.        </file>  
  98.   
  99.        <file>  
  100.   
  101.          <name>Image.aspx</name>  
  102.   
  103.        </file>  
  104.   
  105.        <file>  
  106.   
  107.          <name>module.css</name>  
  108.   
  109.        </file>  
  110.   
  111.      </files>  
  112.   
  113.    </folder>  
  114.   
  115.  </folders>  
  116.   
  117. </dotnetnuke>  


Как видите файлов всего 8, это файлы разметки, css, а так же библиотека сборки. Вы можете произвести Build проекта, собрать указанные файлы в zip архив и установить в DNN. Так же готовый к установке модуль можно найти в исходниках.

Заключение:

В данной статье мы рассмотрели создание простого модуля. Однако, из за большого количества материала, не было затронуто множество других тем, таких как, локализация, настройка модуля и прочее, но базовые знания, полученные здесь, помогут вам в дальнейшем сосредоточиться на достижении поставленной задачи, не теряясь в деталях.

Если вы чего-то не до поняли, или что то вышло не так, то всегда можете скачать файлы проектов и модуля прикрепленные к статье и сравнить их между собой, а так же, создать проект нового модуля и посмотреть его код.

Александр Кобелев aka Megano