Показаны сообщения с ярлыком DNN. Показать все сообщения
Показаны сообщения с ярлыком DNN. Показать все сообщения

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


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

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

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


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