четверг, 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

среда, 16 февраля 2011 г.

Использование PDF шаблонов C#.

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

Продолжая тему шаблонов, хочу раскрыть использование PDF вместе с закрытым исходным кодом. В сети можно найти достаточное количество статей, которые описывают данный процесс, но в основном они используют GNU General Public License библиотеки, а это ведет к открытию своего кода. Проект менеджер услышав об этом, вновь может посылать вас в лес, искать бесплатные отвертки…

Хочу обратить ваше внимание на проект PdfSharp и его лицензию, разрешающую использование в коммерческих целях.

Как использовать шаблоны? Немного теории:

Стандартный подход прост – графически рисуется требуемая форма, расставляются закладки или некие ключевые слова (наподобие @Name@), которые после отыскиваются в документе и заменяются на необходимые значения. Но, к сожалению, PDFsharp не поддерживает парсинг документа, этот подход потерпит фиаско.

Как же быть? Есть два выхода из положения, каждый из них требует больших трудозатрат, чем использование GPL библиотек (можете поискать реализации на iText), первый это полностью генерировать бланки без использования шаблонов, но при сложном бланке на это дело можно потратить чуть ли не целый день на бланк. И второй способ предлагаемый нам PDFsharp, это использовать шаблон как холст для рисования. Представьте, что у нас на столе, под стеклом лежит бланк документа и есть карандаш или маркер, которым мы рисуем на стекле, если стекло достаточно тонкое, то человек, взглянувший на такое "художество", будет считать его единым целым. Далее, мы как раз и будем рисовать на шаблоне, с помощью System.Drawing, подставляя нужные нам данные в требуемые места.

Предположим, в некой мнимой компании «Ключи и Отвертки», для отдела кадров, необходимо создать бланк сотрудника, в котором будут находиться контактные данные, фотография и таблица прихода и ухода на работу за последнюю неделю, и как всегда все это должно быть на фирменном бланке компании.

Не обладая большими талантами дизайнера, я приготовил такой бланк, и самолично его утвердил. :)

c# pdf template

Создадим новое консольное приложение с именем UsingPdfTemplate, далее добавим к проекту ссылки на сборки PdfSharp.dll из каталога GDI+ (заранее скачанные с сайта проекта, на момент написания статьи версия 1_31), а так же на System.Drawing. Добавим к проекту файл template.pdf (взять можно из исходников к статье) и установим его свойство Build Action – Embedded Resource и Copy to Output Directory – Copy always.

Далее введем следующий код:



using System;
using System.Diagnostics;
using PdfSharp.Drawing;
using PdfSharp.Pdf;
using PdfSharp.Pdf.IO;

namespace UsingPdfTemplate
{
class Program
{
static void Main(string[] args)
{
PdfDocument inputDocument = PdfReader.Open("template.pdf", PdfDocumentOpenMode.Import);
PdfPage notEditablePage = inputDocument.Pages[0];

PdfDocument outputDocument = new PdfDocument();
PdfPage editablePage = outputDocument.AddPage(notEditablePage);

outputDocument.Save("newPdf.pdf");
Process.Start("newPdf.pdf");
}
}
}



Мы открываем документ шаблона и находим его первую страницу, после создаем новый документ и помещаем в него данную страницу, после чего ее можно редактировать. Далее без каких либо изменений сохраняем новый документ на диск и запускаем его для просмотра. Получившийся документ – точная копия исходного файла.

Теперь заполним поля «Должность», «Имя», «Email», добавим фотографию и таблицу посещений:




PdfPage editablePage = outputDocument.AddPage(notEditablePage);

XGraphics gfx = XGraphics.FromPdfPage(editablePage);
DrawFields(gfx);
DrawImage(gfx);
DrawTable(gfx);


outputDocument.Save("newPdf.pdf");




Получаем объект XGraphics и передаем его в методы DrawFields(), DrawImage(), DrawTable().

DrawFields отрисует поля на шаблоне, код метода:



private static void DrawFields(XGraphics gfx)
{
XPdfFontOptions fontOptions = new XPdfFontOptions(PdfFontEncoding.Unicode, PdfFontEmbedding.Always);
XFont font = new XFont("Times New Roman", 10, XFontStyle.Bold, fontOptions);

gfx.DrawString("Программист", font, XBrushes.Black, 100, 135);
gfx.DrawString("Александр Кобелев", font, XBrushes.Black, 100, 160);
gfx.DrawString("Kobelev.Alexander@gmail.com", font, XBrushes.Black, 100, 185);
}



Выставляем поддержку юникода и отрисовываем три строчки текста в вымеренных позициях.

Для отрисовки фотографии добавим в проект фото (можно так же взять из исходников фото моего кота), и пометим его свойство Build Action – Embedded Resource и Copy to Output Directory – Copy always.

DrawImage отрисует фотографию, код метода:



private static void DrawImage(XGraphics gfx)
{
XImage img = XImage.FromFile("cat.jpg");
gfx.DrawImage(img, 350, 125, 120, 160);
}



Очень простой метод - мы лишь получаем изображение из файла (XImage можно так же получить, используя System.Drawing.Image, который можно получить из потока) и отрисовываем его на документе по координатам с заданными размерами.

С таблицей будет посложнее, дело в том что PdfSharp ничего не знает о таблицах и способен их нарисовать только из линий, но не стоит падать духом, так как есть замечательная библиотека MigraDoc, которая поставляется вместе с PdfSharp. Нам лишь надо добавить ссылку на библиотеку MigraDoc.DocumentObjectModel.dll и MigraDoc.Rendering.dll.

DrawTable отрисует таблицу, код метода:



...
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Tables;
using MigraDoc.Rendering;

private static void DrawTable(XGraphics gfx)
{

Document doc = new Document();

Table table = new Table();
table.Borders.Width = 0.3;
table.AddColumn(76);
table.AddColumn(78.2);
table.AddColumn(77.6);

for (int day = 7; day > 0; day--)
{
var row = table.AddRow();
row[0].AddParagraph(DateTime.Now.AddDays(-day).ToString("dd/MM/yyyy"));
row[1].AddParagraph(DateTime.Now.AddDays(-day).ToString("HH:mm"));
row[2].AddParagraph(DateTime.Now.AddDays(-day).AddHours(8).ToString("HH:mm"));
}

doc.AddSection().Add(table);

DocumentRenderer docRenderer = new DocumentRenderer(doc);
docRenderer.PrepareDocument();
docRenderer.RenderObject(gfx, 36, 257, 0, table);
}



В этом методе мы вначале создаем MigraDoc.DocumentObjectModel.Document, потом таблицу, выставляем у таблицы ширину бордюра и размеры колонок. После чего заполняем семь строк данными и добавляем таблицу в документ, после создаем объект класса MigraDoc.Rendering.DocumentRenderer, который принимает в качестве параметра конструктора документ содержащий таблицу, подготавливаем и отрисовываем его, используя объект XGraphics переданный в метод.

Как результат у нас появляется заполненный PDF документ на основе шаблона:

c# completed pdf template

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

среда, 9 февраля 2011 г.

Использование WORD шаблонов C# 4.0

Инструментарий:
• Microsoft Visual Studio 2010
• Microsoft Office 2010

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


Данная статья является адаптацией более ранней статьи «Использование WORD шаблонов» с учетом новых возможностей вошедших в C# 4.0 таких как: именованные параметры, пропуск ключевого слова ref при работе с COM, индексируемые свойства и необязательные параметры. Вы можете сравнить количество кода с предыдущей статьей и увидеть, что кода сало много меньше, так же он стал более читабельным.

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

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

Предположим, существует некая компания «Ключи и Отвертки», которая занимается продажами обозначенных инструментов, и менеджеры компании хотят видеть ежедневный отчет о продажах в своих офисах.

Начнем с простого шаблона, который будет представлять из себя фирменный бланк компании и единственное поле для вставки данных. Создадим новый WORD документ и уменьшим отступы (Page Layout -> Margins -> Narrow).


Margins


После этого развернем документ в альбомную ориентацию (Page Layout -> Orientation -> Landscape).

Orientation

Далее создадим шапку (Insert -> Header –> Alpabet).

Footer

Напишем название. Далее впишем дополнительную информацию о компании и пометим ее курсивом с выравниванием по правому краю.

После создания шапки в документ автоматически был добавлен пустой подвал, перейти в редактирование которого можно просто перенеся туда курсор. Визуально отделим его от тела документа при помощи разделительной линии. (Insert –> Shapes -> Lines)

Line

Далее, в подвале сделаем выравнивание по правому краю и создадим закладку (Bookmark), которую будем отыскивать в коде и на ее место вставлять данные (Insert -> Bookmark).

InsertBookmark

И назовем ее AuthorName.

На этом завершим создание фирменного бланка, сохраним шаблон с именем screwdriver.dotx (Word Template)

Следующим шагом создадим из кода новый документ Word, на основе нашего шаблона, впишем автора документа и сохраним его на диск.

Создадим в Visual Studio WPF приложение с названием UsingWordTemplate ( File -> New -> Project -> Windows -> WPF Application).

Добавим на форму Label (Content="Автор:"), TextBox (Name=”autorTxtb”), в который будем вписывать имя автора документа и кнопку (Content="Сохранить" Name=”saveBtn”), с помощью которой будем сохранять созданный документ.

Добавим ссылку на .Net библиотеку Microsoft.Office.Interop.Word Version 14. В шапке кода главного окна добавим


using Word = Microsoft.Office.Interop.Word;


После чего определим переменную


Word._Application oWord = new Word.Application();


Переменная oWord будет представлять процесс WINWORD.EXE в памяти.

Так же определим событие формы Closing, в котором мы будем закрывать процесс WINWORD.EXE


private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
oWord.Quit();
}



Примечание:

В предыдущем фрагменте кода мы незаметно использовали новую возможность C# 4.0 - необязательные параметры. Если сравнить с предыдущим вариантом статьи, то тогда нам потребовалось определить переменную oMissing представляющую из себя аргумент представляющий значение по умолчанию, который требуют методы из пространства Microsoft.Office.Interop. И вызов выглядел следующим образом:


object oMissing = System.Reflection.Missing.Value;

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
oWord.Quit(ref oMissing, ref oMissing, ref oMissing);
}




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

Вызовем контекстное меню текущего проекта и добавим в него заранее созданный шаблон screwdriver.dotx (Add -> Existing Item -> screwdriver.dotx), в его свойствах определим Copy To Output Directory как Copy always.

Определим обработчик кнопки:


private void saveBtn_Click(object sender, RoutedEventArgs e)
{
_Document oDoc = GetDoc(Environment.CurrentDirectory + "\\screwdriver.dotx");
oDoc.SaveAs(FileName: Environment.CurrentDirectory + "\\New.docx");
oDoc.Close();
}


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


Примечание:

Здесь мы вновь используем пропуск ключевого слова ref,необязательные параметры и так же новую возможность – именованные параметры. Для сравнения посмотрим на код которым мы пользовались ранее:

private void SaveToDisk(Word._Document oDoc, string filePath)
{
object fileName = filePath;
oDoc.SaveAs(ref fileName, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing, ref oMissing);
}





Примечание:

Остерегайтесь использования Environment.CurrentDirectory так как значение может измениться в процессе работы.


static void Main(string[] args)
{
Console.WriteLine(Environment.CurrentDirectory);
Environment.CurrentDirectory = "c:\\";
Console.WriteLine(Environment.CurrentDirectory);
}


Как показано выше мы можем сами изменить ее значение, в данном случае Environment.CurrentDirectory больше указывает не на каталог приложения, а на корень диска C.


Ниже приведен код вызванных методов:


private _Document GetDoc(string path)
{
_Document oDoc = oWord.Documents.Add(path);
SetTemplate(oDoc);
return oDoc;
}

private void SetTemplate(Word._Document oDoc)
{
oDoc.Bookmarks["AuthorName"].Range.Text = autorTxtb.Text;
}


Сигнатуры вызываемых методов из пространства имен Microsoft.Office.Interop можно посмотреть по адресу:

http://msdn.microsoft.com/ru-ru/library/microsoft.office.interop.word%28en-us%29.aspx

Введем в текст бокс данные автора документа “Александр Кобелев” и сохраним документ на диск нажав кнопку “Сохранить”. В папке Debug нашего приложения появится файл New.docx, содержащий введенные данные на месте bookmark AuthorName.

Теперь добавим еще одну кнопку (Content="Print" Name="prntBtn") на форму, которая будет распечатывать документ на принтер и определим ее обработчик.


private void prntBtn_Click(object sender, RoutedEventArgs e)
{
_Document oDoc = GetDoc(Environment.CurrentDirectory + "\\screwdriver.dotx");
PrintDoc(oDoc);
oDoc.Close(WdSaveOptions.wdDoNotSaveChanges)
}


Здесь все то же самое, что и в предыдущем случае, только вместо метода SaveToDisk вызывается PrintOut() и перед закрытием документа явно определяется, что документ не стоит сохранять. В предыдущем случае, при сохранении документа на диск, он помечался как сохраненный и не требовалось явно указывать этот параметр.

Теперь добавим на шаблон таблицу 3 на 4, которая будет отображать продажи по городам. Верхняя строчка обозначит ключи и отвертки, а первый столбик города: Москва, Санкт-Петербург, Челябинск. Разукрасим таблицу по своему вкусу.

table

Далее мы могли бы поставить шесть закладок (boomark), задать им уникальные имена и искать их как в предыдущем случае, но это слишком накладно. Можно поступить проще – выделить всю таблицу и добавить закладку на все выделение сразу.

selectTable

Назовем новую закладку “SaleTable”.

Добавим на форму DataGrid, со свойством AutoGenerateColumns="True", а в код новую переменную

В конструкторе вызовем метод, который заполнит DataTable и свяжет его с DataGrid


public MainWindow()
{
InitializeComponent();
InitializeDataGrid();
}

private void InitializeDataGrid()
{
table = new DataTable();
table.Columns.Add();
table.Columns.Add();
table.Columns.Add();
var row = table.NewRow();
row[0] = "Город";
row[1] = "Ключи";
row[2] = "Отвертки";
table.Rows.Add(row);
table.Rows.Add(table.NewRow()[0] = "Москва");
table.Rows.Add(table.NewRow()[0] = "Санкт-Петербург");
table.Rows.Add(table.NewRow()[0] = "Челябинск");
dataGrid1.DataContext = table;
}


Добавим в метод SetTemplate() вызов метода SetTable() который будет заполнять данными таблицу.


private void SetTemplate(Word._Document oDoc)
{
. . .
SetTable(oDoc,"SaleTable",table);
}

private void SetTable(Word._Document oDoc, string bookmark, DataTable dataContext)
{
dynamic tbl = oDoc.Bookmarks[bookmark].Range.Tables[1];
int tblRow = 0;
int tblCell = 0;
foreach (Word.Column col in tbl.Columns)
{
foreach (Word.Cell cell in col.Cells)
{
SetCell(cell, (string)dataContext.Rows[tblRow][tblCell]);
tblRow++;
}
tblCell++;
tblRow = 0;
}
}


Как видно из листинга, мы перебираем по очереди все ячейки в шаблоне и сопоставляем их с данными из DataTable.

Продолжая развивать идею предположим, что менеджерам компании было бы удобно, если бы при очень низких значениях продаж (меньше 100), в отчете это отмечалось красной ячейкой, сразу бросающейся в глаза и наоборот, если уровень продаж высок (больше 500), ячейка помечалась зеленым цветом.

Для реализации немного изменим метод SetTable() , заменив явное присваивание на вызов метода SetCell().


...
foreach (Word.Cell cell in col.Cells)
{
SetCell(cell, (string)dataContext.Rows[tblRow][tblCell]);
tblRow++;
}
...

private void SetCell(Word.Cell cell, string text)
{
int val = 0;
if (int.TryParse(text, out val))
{
if (val < 100) cell.Shading.BackgroundPatternColor = Word.WdColor.wdColorRose;
if (val > 500) cell.Shading.BackgroundPatternColor = Word.WdColor.wdColorLightGreen;
}
cell.Range.Text = text;
}


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

Перейдем к графикам.

Для использования графиков потребуется ссылка на .Net библиотеку Microsoft.Office.Interop.Excel Version 14. Так же в шапке кода главного окна добавим using


using Excel = Microsoft.Office.Interop.Excel;


Сделаем несколько отступов вниз, для создания пространства между таблицей и графиком, добавим в шаблон график (Insert -> Chart -> Column -> 3d Column)

insertChart

3dColumn

и выставим все значения по нулям.

chartDefValue

После чего выделим график на шаблоне (кликнув по краю графика) и создадим новую закладку “ChartBookmark”, далее сохраним изменения в шаблоне.

Добавим в метод SetTemplate() вызов метода SetChart(), который будет заполнять данными график.


private void SetTemplate(Word._Document oDoc)
{
...
SetChart(oDoc, "ChartBookmark", table);
}

private void SetChart(Word._Document oDoc, string bookmark, DataTable dataContext)
{
Word.Chart chart = oDoc.Bookmarks[bookmark].Range.InlineShapes[1].Chart;
Word.ChartData chartData = chart.ChartData;
chartData.Activate();

Excel.Workbook dataWorkbook = (Excel.Workbook)chartData.Workbook;
Excel.Worksheet dataSheet = (Excel.Worksheet)dataWorkbook.Worksheets[1];
dataSheet.Cells.Range["B2"].FormulaR1C1 = dataContext.Rows[1][1];
dataSheet.Cells.Range["B3"].FormulaR1C1 = dataContext.Rows[1][2];
dataSheet.Cells.Range["C2"].FormulaR1C1 = dataContext.Rows[2][1];
dataSheet.Cells.Range["C3"].FormulaR1C1 = dataContext.Rows[2][2];
dataSheet.Cells.Range["D2"].FormulaR1C1 = dataContext.Rows[3][1];
dataSheet.Cells.Range["D3"].FormulaR1C1 = dataContext.Rows[3][2];
dataWorkbook.Close();
}


Теперь приложение полностью готово к работе, введем некоторые данные, чтобы провести функциональный тест. В строке автор я введу “Александр Кобелев” а значения продаж по городам у меня будут такие: Ключи/Отвертки Москва – 700/400, Санкт-Петербург – 50/300, Челябинск – 100/200. После нажатия кнопки “Сохранить” в папке Debug приложения появился файл New.docx, такого вида:

end

Как видно из статьи, изменения вышедшие в C# 4.0 - намного сокращают код необходимый для работы с СОМ, в частности при работе с Office.

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

воскресенье, 14 ноября 2010 г.

Time Triangles

Данная статья - мое ИМХО, только ИМХО и ничего кроме ИМХО =)

   Хочу поделиться своими ощущениями по данной теме, начнем с термина Time Triangles – временные треугольники. Так что же это такое, что с ними можно делать и в чем их польза?

   Time Triangles позволяют объяснить почему на задачу которую вы предполагали потратить пол часа в реальности было потрачено много больше времени, а так же они позволяют произвести самооценку профессиональных навыков по любому направлению, и так встречаем – Time Triangles.


   Что же объясняет представленная выше диаграмма? Самый верхний отрезок «Статья №1» обозначен цифрой 30. Тут все просто - к примеру мы имеем некий журнал в нем есть тематическая статья, в среднем на прочтение статей данного объема у меня уходит минут 30, следовательно прочитать данную статью я планирую за 30 минут. Далее предположим что в момент прочтения автор статьи затрагивает неизвестную мне тему, и вполне вероятно ссылается на свою работу или другого автора опубликованную где то еще. Так как я не могу продолжать чтение статьи из-за недостатка знаний, необходимых для дальнейшего осмысления материала, то мне приходится отыскать указанную статью и приступить к ее чтению, чтобы в последствии вернутся к первой статье. Отыскав эту статью я вижу что ее объем примерно равен объему первой статьи и приблизительно я на нее потрачу так же 30 минут, таким образом появляется первый треугольник.



   Но с первых строк автор второй статьи предостерегает, что он не будет описывать базовые понятия (так же еще не знакомые мне), но эти знания необходимы для осознания этой статьи. Далее через поисковую систему мне удалось найти вебкаст по основам данной предметной области и этот вебкаст длится ровно 68 минут.





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




   И только после прочтения третьей статьи я сумел просмотреть вебкаст и понять его, но так как материала в вебкасте оказалось достаточно много, мною было принято решение просмотреть его еще раз. После повторного просмотра я смог вернуться к статье номер два, а потом к статье номер один и благополучно дочитать ее до конца с полным осознанием материала, самая первая диаграмма как раз полностью описывает данный процесс. Теперь можно наглядно объяснить почему на прочтение статьи было потрачено более четырех часов, вместо запланированных тридцати минут. Так же данная диаграмма показывает что у меня довольно низкий уровень знаний по данному направлению, так как если бы я знал основы то как минимум мне не пришлось читать статью номер три и смотреть вебкасты, а в наилучшем варианте, я бы просмотрел статью по диагонали за 10 минут, отлично понимая материал без единого Time Triangles.



   Ну и напоследок, хочу пожелать вам поменьше встречаться с Time Triangles в жизни, так как чем меньше их видишь, тем больше осознаешь, что чего-то ты уже сумел добиться по данному направлению =)


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