Было найдено интересное тестовое задание раскрывающее суть динамического подключения сборок на .NET платформе.
Тестовое задание звучит так:
"Разработать WinForms приложение. Основное окно должно содержать грид для отображения списка объектов. На форме должны быть кнопки: добавить, изменить, удалить. Кнопки должны открывать диалоговые формы для ввода данных, изменения данных и подтверждения удаления. Кнопка добавления должна предварительно открывать список классов объектов для выбора класса создаваемого объекта (например: физ. лицо, юр. лицо, инд. предприниматель).
Приложение не должно знать заранее список классов объектов.
Настройки хранить в файле Options.xml. Каждый класс сущности реализовать в динамически подключаемой библиотеке. В файле Options.xml указать список классов и ДЛЛ, которые их реализуют. Для каждого класса своя ДЛЛ и своя форма для создания/изменения, но одна форма на удаление объекта любого класса. Свойства объектов хранить в xml-файле. После обработки список в гриде должен обновляться."
Мне оно показалось интересным в первую очередь тем, что я редко, да и думаю что любой другой программист не часто, используют подобную фичу в своих проектах, хотя в Self подготовке к 70-536 эта тема описывается.
Ну в общем перейдем к процессу решения этой задачи. Для начала я прикинул что?где? когда? Почитал немного доков от мелколегких здесь. И решил что структура проекта будет следующая:
1. WinForms приложение - приложение подключающее библиотеки - как сущности и отображающее их объекты.
2. Связующая библиотека - аля ядро, содержащая необходимые интерфейсы для Сущности и ее класса (самое главное тут).
3. Множество различных сборок, удовлетворяющие интерфейсам из ядра.
Немного теории о доменах (не DNS а о Application доменах) и сборках можно почитать тут .
Сказано - сделано:
где:
DynamicObject_t - WinForms
EntityLayer - ядроTestEntity1 и TestEntity2 - множество библиотек.
По условию задачи в WinForms приложении у нас 2-е формы:
1. Основная - с тремя "кнопыщками" и "гридом".
2. Для выбора типа (сущности, класса) добавляемого объекта.
Снова, сказано - сделано:
Но это все косметика, самое интересное это что же из себя представляет EntityLayer.
EntityLayer содержит два интерфейса: 1. IClassEntity.cs - отображает класс для работы со сборкой, 2. - IObject.cs отображает класс объектов сборки. И класс XmlHelper.cs - содержит функционал сериализации и десериализации объектов сборки в Xml.
,где IClassEntity.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace EntityLayer
{
public interface IClassEntity
{
string Name { get; } //Имя сборки
Form Edit(int ID); //Возвращает объект формы для редактирования, объекта
Form Add();//Возвращает объект формы для добавления нового объекта
void Delete(int ID); //Удаляет объект
List<IObject> Objects { get;}//Возвращает список объектов сборки
}
}
,а - IObject.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EntityLayer
{
public interface IObject
{
int ID {get; set; }//Идентификатор объекта в сборке
string ObjectType { get;}//Имя сборки
string Summary { get; } // Общее поле, возвращает суммарное значение полей объекта.
}
}
Как мы будем это делать в нашем WinForms приложении?
Для этого в этом проекте я создал класс Options.Helper.cs со статическим методом GetEntities, он считывает заказанный нам в условии Options.xml со сборками и классами и записывает уже интерпретированные классы сборок в List<EntityLayer.IClassEntity>
public static List<EntityLayer.IClassEntity> GetEntities(out bool IsError)
{
List<EntityLayer.IClassEntity> list = new List<EntityLayer.IClassEntity>();
IsError = false;
try
{
XElement root = Element.Load(Properties.Settings.Default.OptionFile);
var Ents = root.Elements();
foreach (var h in Ents)
{
list.Add(GetEntity(h.Attribute(XName.Get("class")).Value, h.Attribute(XName.Get("path")).Value));
}
}
catch(Exception ex)
{
Log.WriteErrorLine(ex.ToString());
IsError = true;
}
return list;
}
static EntityLayer.IClassEntity GetEntity(string ClassName, string
AssemblyFile)
AssemblyFile)
{
if(!File.Exists(AssemblyFile)) throw new FileNotFoundException("Can't
find assembly file.");
find assembly file.");
Assembly assembly = Assembly.LoadFile(Application.StartupPath + "\\"
+ AssemblyFile);
+ AssemblyFile);
return (EntityLayer.IClassEntity)Activator.CreateInstance(assembly.GetType(ClassName));//Именно здесь происходит загрузка инициированного объекта класса со сборки, и оборачивание его в IClassEntity интерфейс.
}
Ок, теперь ясно как это считывается и интерпретируется, а как выглядит класс самой подключаемой сборки? Пожалуйста...using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EntityLayer; namespace TestEntity1
{
public class CompanyEntity:IClassEntity
{
List<Company> Companies;//Список наших объектов, в данном случае компании
XmlHelper<Company> helper;
public const string ObjectXmlFile = "TestEntity1.xml"; //Файл для сериализации и обратного процесса
public string Name
{
get{ return "Company";}
}
{
helper = new XmlHelper<Company>(CompanyEntity.ObjectXmlFile);
Companies = helper.Deserialise();
}
public System.Windows.Forms.Form Edit(int ID)
{
return new EditCompany(ID,Companies);//Вовращение формы для редактирования, реализуемой в данной сборке
}
public System.Windows.Forms.Form Add()
{
return new EditCompany(null, Companies);
}
public List<IObject> Objects//Возвращение объектов
{
get
{
List<IObject> result = new List<IObject>();
foreach(var h in Companies)
result.Add(h);
return result;
}
}
public void Delete(int ID)//Удаление объекта
{
var h = Companies.Where(i=>i.ID==ID).FirstOrDefault();
if(h != null)
{
Companies.Remove(h);
helper.Serialise(Companies);
}
}
}
}
Ну вот в общем то и все. На первый взгляд может показаться муторно, но думаю темка прикольная, особенно будет актуальна при создании различных Plugin конфигураций для вашего приложения.
Полный код проекта: SVN Тестового Проекта (как хорошо что есть дядя Гугл).