четверг, 18 августа 2011 г.

IStructuralEquatable & IStructuralComparable


IStructuralEquatable & IStructuralComparable
Редакция статьи от 24.04.2013
   С выходом .net 4, в нем помимо всего прочего, добавилось два интерфейса - IStructuralEquatable & IStructuralComparable, которые явно (explicit) реализуются кортежами (Tuple) и массивами (Array).
IStructuralEquatable
 
Позволяет структурно сравнить между собой два объекта.
public interface IStructuralEquatable
{
   bool Equals(object other,System.Collections.IEqualityComparer comparer);
   int GetHashCode(System.Collections.IEqualityComparer comparer);
}

   К примеру, необходимо сравнить два Int32 массива. Структура массивов, представляет из себя последовательность значений, соответственно, чтобы массивы считались равными нужно сравнить между собой значения массивов, то есть первый элемент первого массива должен быть равен первому элементу второго массива, и так далее. Если попробовать выполнить эту задачу в лоб, то столкнёмся с некоторыми трудностями, так как по умолчанию поведение метода System.Object.Equals проверяет, указывают ли ссылки на один и тот же объект, и System.Array не переопределяет данный метод.
using System;

namespace Structual
{
    class Program
    {
        static void Main()
        {
            int[] array = { 1, 1, 8 };
            int[] anotherArray = { 1, 1, 8 };

            Console.WriteLine(array.Equals(anotherArray));// False
        }
    }
}

   Чтобы структурно сравнить два массива, надо создать класс, реализующий IEqualityComparer, этот интерфейс используется для сравнения элементов массивов друг с другом. К примеру если в сравниваемых массивах по три элемента то этот интерфейс будет использован трижды, с его помощью в начале будут сравнены два первых элемента массивов, затем два вторых и два третьих.
using System;
using System.Collections;

namespace Structual
{
    public sealed class ObjectEqualityComparer : IEqualityComparer
    {
        public new bool Equals(object itemFromArray1, object itemFromArray2)
        {
            return itemFromArray1.Equals(itemFromArray2);
        }
        public int GetHashCode(object obj)
        {
            return obj.GetHashCode();
        }
    }
    class Program
    {
        static void Main()
        {
            int[] array = { 1, 1, 8 };
            int[] anotherArray = { 1, 1, 8 };
            IStructuralEquatable firstArreyForEqual = array;
            Console.WriteLine(firstArreyForEqual.Equals(anotherArray, new ObjectEqualityComparer())); //True 
        }
    }
}

Обратите внимание на строку кода
IStructuralEquatable firstArreyForEqual = array;

   Интерфейс IStructuralEquatable реализован в System.Array как Explicit Interface и мы не можем обратиться к нему напрямую через переменную array.
   Так же, постоянно реализовывать ObjectEqualityComparer нет необходимости, так как в .net уже есть специальный класс  StructuralEqualityComparer, если воспользоваться дизассемблером и взглянуть на его код, то увидим следующее:
internal class StructuralEqualityComparer : IEqualityComparer
{
    // Methods
    public bool Equals(object x, object y)
    {
        if (x != null)
        {
            IStructuralEquatable equatable = x as IStructuralEquatable;
            if (equatable != null)
            {
                return equatable.Equals(y, this);
            }
            return ((y != null) && x.Equals(y));
        }
        if (y != null)
        {
            return false;
        }
        return true;
    }

    public int GetHashCode(object obj)
    {
        if (obj == null)
        {
            return 0;
        }
        IStructuralEquatable equatable = obj as IStructuralEquatable;

        if (equatable != null)
        {
            return equatable.GetHashCode(this);
        }
        return obj.GetHashCode();
    }
}
   Как можно заметить функциональность у нашего ObjectEqualityComparer и StructuralEqualityComparer идентична (за исключением проверок, которые необходимы для кода использующегося в реальных коммерческих приложениях), можно переписать предыдущий пример.
  Так как класс StructuralEqualityComparer имеет уровень доступа Internal, получить его можно через статический класс StructuralComparisons:
using System;
using System.Collections;

namespace Structual
{
    class Program
    {
        static void Main()
        {
            int[] array = { 1, 1, 8 };
            int[] anotherArray = { 1, 1, 8 };
            IStructuralEquatable firstArreyForEqual = array;
            Console.WriteLine(firstArreyForEqual.Equals(anotherArray, StructuralComparisons.StructuralEqualityComparer)); //True 
        }
    }
}

Обратите внимание на код в StructuralEqualityComparer
IStructuralEquatable equatable = x as IStructuralEquatable;
if (equatable != null)
{
    return equatable.Equals(y, this);
}

   Он учитывает, является ли сравниваемый элемент сам массивом, кортежем или любым другим типом реализующим IStructuralEquatable, к примеру:
using System;
using System.Collections;

namespace Structual
{
    class Program
    {
        static void Main()
        {
            Object[] array = { 1, new[] {1,2,3} };
            Object[] anotherArray = { 1, new[] { 1, 2, 3 } };
            IStructuralEquatable firstArreyForEqual = array;
            Console.WriteLine(firstArreyForEqual.Equals(anotherArray, StructuralComparisons.StructuralEqualityComparer)); //True 
            ((int[]) anotherArray[1])[2] = 5;
            Console.WriteLine(firstArreyForEqual.Equals(anotherArray, StructuralComparisons.StructuralEqualityComparer)); //False
        }
    }
}
   Но как же происходит процесс сравнения?  Посмотрим через дизассемблер на реализацию интерфейса IStructuralEquatable в System.Array:
bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
{
    if (other == null)
    {
        return false;
    }
    if (!object.ReferenceEquals(this, other))
    {
        Array array = other as Array;
        if ((array == null) || (array.Length != this.Length))
        {
            return false;
        }

        for (int i = 0; i < array.Length; i++)
        {
            object x = this.GetValue(i);
            object y = array.GetValue(i);
            if (!comparer.Equals(x, y))
            {
                return false;
            }
        }
    }
    return true;
}

int IStructuralEquatable.GetHashCode(IEqualityComparer comparer)
{
    if (comparer == null)
    {
        throw new ArgumentNullException("comparer");
    }
    int num = 0;
    for (int i = (this.Length >= 8) ? (this.Length - 8) : 0; i < this.Length; i++)
    {
        num = CombineHashCodes(num, comparer.GetHashCode(this.GetValue(0)));
    }
    return num;
}
   Если в двух словах описать реализацию IStructuralEquatable.Equals в System.Array, то она поочередно сравнивает все элементы в массивах и если хоть один из них не равен друг другу, то и массивы не равны.
IStructuralComparable

Поддерживает структурное сравнение объектов коллекции.
   Все выше сказанное так же относится и к IStructuralComparable, он так же реализован в
System.Array и Sytem.Tuple<T> как Explicit Interface. И для него так же реализован класс StructuralEqualityComparer, который можно получить через статический класс StructuralComparisons, для сравнения массивов и кортежей.
Рассмотрим на примере:

using System;
using System.Collections;

namespace Structual
{
    internal class Program
    {
        private static void Main()
        {
            int[] array = { 1, 1, 8 };
            int[] anotherArray = { 1, 1, 8 };

            IStructuralComparable firstArrey = array;

            Console.WriteLine(firstArrey.CompareTo(anotherArray, StructuralComparisons.StructuralComparer));// результат 0, означает что массивы равны
            anotherArray[2] = 7;
            Console.WriteLine(firstArrey.CompareTo(anotherArray, StructuralComparisons.StructuralComparer));// результат 1, означает что firstArray больше чем anotherArray
            anotherArray[2] = 9;
            Console.WriteLine(firstArrey.CompareTo(anotherArray, StructuralComparisons.StructuralComparer));// результат -1, означает что firstArray меньше чем anotherArray
        }
    }
}

Исследуем с помощью дизассемблера реализацию  IStructuralComparable.CompareTo в System.Array:
int IStructuralComparable.CompareTo(object other, IComparer comparer)
{
    if (other == null)
        return 1;

    Array array = other as Array;
    if (array == null || this.Length != array.Length)
        throw new ArgumentException(Environment.GetResourceString("ArgumentException_OtherNotArrayOfCorrectLength"), "other");
   
    int index = 0;
    int num;
    for (num = 0; index < array.Length && num == 0; ++index)
    {
        object x = this.GetValue(index);
        object y = array.GetValue(index);
        num = comparer.Compare(x, y);
    }
    return num;
}

Как видно из кода, метод пытается найти первое расхождение, если расхождений не обнаружено - объекты равны.

Теперь рассмотрим под дизассемблером Код StructuralComparer:

internal class StructuralComparer : IComparer
{
    public int Compare(object x, object y)
    {
        if (x == null)
        {
            if (y != null)
                return -1;
            else
                return 0;
        }
        else
        {
            if (y == null)
                return 1;
            IStructuralComparable structuralComparable = x as IStructuralComparable;
            if (structuralComparable != null)
                return structuralComparable.CompareTo(y, (IComparer)this);
            else
                return Comparer.Default.Compare(x, y);
        }
    }
}

Довольно интересна следующая часть кода:
IStructuralComparable structuralComparable = x as IStructuralComparable;
if (structuralComparable != null)
    return structuralComparable.CompareTo(y, (IComparer)this);
  Это означает, если элемент массива, сам является массивом, кортежем или любым другим типом реализующим IStructuralComparable, то сравнивание будет это учитывать.
К примеру:
using System;
using System.Collections;

namespace Structual
{
    internal class Program
    {
        private static void Main()
        {
            object[] array = { 1, new[] {1,2,3}};
            object[] anotherArray = { 1, new[] { 1, 2, 3 } };

            IStructuralComparable firstArrey = array;

            Console.WriteLine(firstArrey.CompareTo(anotherArray, StructuralComparisons.StructuralComparer));// результат 0, означает что массивы равны
            ((int[])anotherArray[1])[2] = 2;
            Console.WriteLine(firstArrey.CompareTo(anotherArray, StructuralComparisons.StructuralComparer));// результат 1, означает что firstArray больше чем anotherArray
            ((int[])anotherArray[1])[2] = 4;
            Console.WriteLine(firstArrey.CompareTo(anotherArray, StructuralComparisons.StructuralComparer));// результат -1, означает что firstArray меньше чем anotherArray
        }
    }
}

   На этом все, удачи вам и спасибо что прочитали мою статью. Буду рад оставленным комментариям и новым постоянным читателям.

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

вторник, 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