محتويات المقالة :
- بناء وتكوين اطار عمل MEF) Managed Extensibility Framework).
- الContracts.
- استيراد وتصدير الإضافات (parts).
- الContainers.
- الفهارس (Catalogs).
تسمح لنا الإضافات (أو ما تعرف بAdd-ins أو Plugins) باضافة مميزات ووظائف جديدة إلى تطبيقاتنا في أي وقت نريده . وهي منتشرة بشكل كبير في هذه الأيام حيث أن اغلب البرامج التي نراها الان تحتوي على اضافات يمكن للمستخدم التحكم بها, فمثلاً لو نظرنا إلى متصفح الإكسبلورر فسنجد به كثير من الإضافات مثل تولبار جووجل الذي يسمح لنا بالوصول لمحرك البحث بسهولة من المتصفح . وهناك اضافات اخرى مثل محرك الفلاش shockwave Flash الذي يسمح بعرض صفحات الويب التي تحتوي على مكونات فلاشية . ومن الامثلة الاخرى على البرامج التي تسمح بالإضافات الفيجوال ستديو الذي يحتوي على نموذج خاص بالإضافات يسمح لنا باضافة خواص متعددة اليه .
الفائدة الأكبر للتطبيقات المرنة هي أنها تسمح لمبرمجين اخرين بأن يطوروا وظائف ومميزات جديدة لبرامجك فيمكنك انشاء نموذج اضافات مخصص لبرنامجك يمتلك القدرة على تحميل مميزات معينة من الassemblies . وحتى تفعل ذلك تحتاج اولاً لتفهم عدة قضايا مثل طريقة ايجاد الإضافات وطريقة استعمالها , وهذه المهمة يمكن أن تتم بشكل تلقائي عن طريق Managed Extensibility Framework أو MEF .
قبل أن نبدأ بالتعرف على الامور السابقة يجب أن نفهم اولاً بعض المصطلحات التي سنستخدمها :
فمثلا نسمي الإضافة (add-in أو plugin أو extension) داخل اطار عمل MEF بالpart . يتم تعريف الpart كexport عندما يتم برمجتها وبعد ذلك يتم اضافتها داخل الcontainer والذي هو مكان وضع الإضافة داخل البرنامج والذي يقوم بعمل import للparts ليتم اضافتها للبرنامج . وقبل أن يقوم بعمل import لها فيجب على الcontainer أن يقوم بايجاد الparts عن طريق catalog ما . حيث يقوم الأخير بعرض جميع الparts المتوفرة .
نسمي البرنامج المرن الذي يقبل الإضافات بالبرنامج أو التطبيق المستضيف hosting application .
هذا هو ملخص سريع لطريقة عمل الإضافات ضمن اطار عمل MEF الجديد . وسندرس في هذه المقالة مثالين يوضحان كيفية العمل مع MEF .
المثال الأول هو عبارة عن برنامج حاسبة من نوع كونسول يقوم بتحميل العمليات الحسابية من الإضافات , اما المثال التالي فهو عبارة عن برنامج حاسبة مبني بالWPF يقوم بتحميل عدة اضافات مثل حسابات الأطوال والزمن ودرجات الحرارة كما في الصورة التالية :

سنركز في هذه المقالة على الnamespace التي تسمى : System.ComponentModel.Composition.
بناء وتكوين اطار عمل MEF
يقدم لنا اطار عمل الدوت نت 4 تقنيتان رائعتان لبرمجة التطبيقات المرنة عن طريق تحميل الإضافات بكل ديناميكي .
التقنية الاولى والتي سنتحدث عنها في هذه المقالة وهي MEF) Managed Extensibility Framework) أما التقنية الاخرى والتي هي متوفرة منذ .NET 3.5 وهي Managed Add-in Framework أو ما نختصرها عادة بMAF والتي سنناقها في مقالة اخرى ان شاء الله .
تستعمل MAF خطوط اتصال من اجل التواصل بين الإضافاة Add-in والتطبيق المستضيف والذي يجعل عملية التطوير اكثر تعقيداً لكن فائدته أن يقدم فصل مميز للاضافات داخل الapplication domains أو حتى على المعالجات المختلفة . ولهذا الأمر تعتبر MEF اسهل التقنيات لانشاء تطبيقات مرنة .
معلومة : يمكن دمج الMEF والMAF معاً من اجل الحصول على مزايا التقنيتان معاً في ان واحد لكن هذا يحتاج لعمل اكثر من طرفك.
تتكون MEF من الparts والcontainers كما هو مبين في الشكل التالي :

يقوم الcontainer بايجاد الparts من catalog ما ويقوم الأخير بايجاد الparts ضمن اسمبلي أو مجلد directory معين.
يقوم الcontainer ايضاً بوصل الimported parts مع الexported parts وبالتالي يجعل هذه الparts متوفرة للتطبيق المستضيف.
يوضح المخطط السابق كيفية تحميل الparts حيث يمكن ايجادها ضمن catalog ما ويستعمل هذا الcatalog الexports من اجل ايجاد الparts التي تخصه .
يقوم الexport provider بالدخول للcatalog من اجل تزويد الexports من الcatalog . ويمكن وصل عدة export providers على شكل سلاسل من اجل عمل exports مخصصة مثلاً (وذلك من اجل السماح فقط لparts مستخدمين معينيين أو حسب قوانين ما ).
يستعمل الcontainer الexport providers من اجل وصل الimports مع الexports وهو نفسه عبارة عن export provider.
تتكون MEF من ثلاث مجموعات ضخمة من الكلاسات :
- كلاسات الإستضافة : وتتضمن الcatalogs والcontainers.
- كلاسات primitives : وتستعمل كbase classes من اجل توسعة بناء وتكوين MEF حتى تستعمل تقنيات اخرى من اجل الوصل بين الimports والexports.
- كلاسات لالية attribute-based : وهي الكلاسات التي تجعل الimplementation لالية attribute-based عن طريق الreflection . (مثل الattributes : Import و Export ).
ويجود هناك كلاسات اخرى جزء من MEF وهي التي تقدم الextension methods والتي تسهل العمل مع الattribute-based parts.
معلومة : يعتمد الimplementation لMEF على الattributes التي تضع اشارة على الparts التي يجب عمل export لها وتقابلها بالimports.
يجب أن تعلم أن هذه التقنية مرنة بمعنى أنها تسمح لاليات اخرى لكي يتم عمل implement لها عن طريق استعمال كلاس الattribute الأساسي ComposablePart و extension methods مع اليات تعتمد على الreflection من كلاس ReflectionModelServices .
يمكن توسعة هذا البناء إلى الكلاسات المشتقة من ComposablePart وتقديم extension methods اضافية تقوم باستقبال المعلومات .
لنبدأ عملية توضيح بناء وتكوين MEF عن طريق مثال بسيط وهو تطبيق كونسول يمثل حاسبة مستضيفة تقوم باضافة الparts من مكتبة معينة حيث تمثل هذه الparts العمليات الحسابية المدعومة من قبل الحاسبة .
حتى يكون عندنا استقلالية بين الحاسبة واضافاتها فسوف ننشأ ثلاثة assemblies : الأسمبلي الاولى هي SimpleContract والتي تضع الشروط والقواعد المستعملة من قبل الadd-in والتطبيق المستضيف.
تقوم اسمبلي الadd-in المسماة SimpleCalculator بعمل implements للcontract المعرفة من قبل اسمبلي الcontract .
تستعمل الحاسبة اسمبلي الcontract من اجل استدعاء الadd-ins.
يتم تعريف الcontracts في الأسمبلي SimpleContract عن طريق اثنين من الinterfaces وهما ICalculator و IOperation حيث يعرف الأول الmethods : GetOperation() و Operate() .
تقوم ميثود GetOperation() بارجاع قائمة لكافة العمليات التي يدعمها الadd-in للحاسبة , ويتم استدعاء العملية عن طريق الميثود Operate() . لاحظ أن هذا الإنترفيس يمتاز بالمرونة حيث يمكن الحاسبة من دعم عدة عمليات مختلفة . أما اذا قام هذا الإنترفيس بتعريف الmethods : Add() و Subtract() بدلاً من ميثود Operate() المرن على سبيل المثال فإننا بحاجة لانشاء نسخة جديدة من هذا الإنترفيس من اجل دعم ميثود Divide() وميثود Multiply() .
اما الطريقة التي قمنا بها بتعريف الإنترفيس ICalculator فتقدم لنا امكانية دعم الحاسبة لأي عدد من العمليات بأي رقم من الoperands .
//انظر للملف SimpleContract/ICalculator.cs
using System.Collections.Generic;
namespace Bukhari.MEF
{
public interface ICalculator
{
IList <IOperation > GetOperations();
double Operate(IOperation operation, double[] operands);
}
}يستعمل الإنترفيس ICalculator الإنترفيس IOperation من اجل ارجاع قائمة العمليات ومن اجل استدعاء عملية ما .
يعرف الإنترفيس IOperation الread-only properties : Name و NumberOperands .
//انظر للملف SimpleContract/IOperation.cs
namespace Bukhari.MEF
{
public interface IOperation
{
string Name { get; }
int NumberOperands { get; }
}
}لا يحتاج الأسمبلي SimpleContract لأي رفرنس لMEF Assemblies حيث أنه يحتوي على interfaces بسيطة من .NET فقط .
تحتوي اسمبلي الadd-in المسماة SimpleCalcultor على كلاسات تعمل implement للinterfaces المعرفة من قبل الcontracts . يقوم كلاس Operation بعمل implement للانترفيس IOperation حيث يحتوي هذا الكلاس على اثنتين من الproperties فقط مثل الإنترفيس .
يعرف ذلك الإنترفيسget للproperties ويعرف ايضاً internal set من اجل وضع قيم للproperties من داخل الأسمبلي .
//انظر للملف SimpleContract/Operation.cs
namespace Bukhari.MEF
{
public class Operation : IOperation
{
public string Name { get; internal set; }
public int NumberOperands { get; internal set; }
}
}يقدم كلاس Calculator وظائف هذه الadd-in عن طريق عمل implement للانترفيس ICalculator. ويتم عمل export للكلاس Calculator كpart كما هو مبين مع الattribute المسماة Export والتي هي معرفة في الnamespace : System.ComponentModel.Composition في الأسمبلي System.ComponentModel.Composition
//انظر للملف SimpleContract/Calculator.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
namespace Bukhari.MEF
{
[Export(typeof(ICalculator))]
public class Calculator : ICalculator
{
public IList <IOperation > GetOperations()
{
return new List <IOperation >()
{
new Operation {]"+", NumberOperands=2},
new Operation {]"-", NumberOperands=2},
new Operation {]"/", NumberOperands=2},
new Operation {]"*", NumberOperands=2}
};
}
public double Operate(IOperation operation, double[] operands)
{
double result=0;
switch (operation.Name)
{
case "+":
result=operands[0] + operands[1];
break;
case "-":
result=operands[0] - operands[1];
break;
case "/":
result=operands[0] / operands[1];
break;
case "*":
result=operands[0] * operands[1];
break;
default:
throw new InvalidOperationException(
String.Format("invalid operation {0}", operation.Name));
}
return result;
}
}
}بشكل عام تستعمل الadd-in الattribute المسماة Export من اجل التعرف على ما الذي سوف يصدر , وتستعمل الattribute المسماة Import من اجل التعرف على ماالذي سوف يستعمل من تلك الإضافات داخل التطبيق المستضيف .
لاحظ أننا قمنا بوضع الattribute المسماة Import على الproperty : Calculator والتي تعمل get و set لobject يعمل implement ل ICalculator . لذلك أي add-in للحاسبة تعمل implement لهذا الإنترفيس فيمكن استخدامها هنا .
//انظر للملف SimpleHost/Program.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using Bukhari.MEF.Properties;
namespace Bukhari.MEF
{
class Program
{
[Import]
public ICalculator Calculator { get; set; }يتم انشاء instance جديد من كلاس Program داخل ميثود Main() لتطبيق الكونسول وبعدهها يتم استدعاء الميثود Run() وفي تلك الميثود يتم انشاء DirectoryCatalog والذي هو مهيئ مع AddinDirectory والذي هو معدل في ملف اعدادات التطبيق .
يعتبر كلاس CompositionContainer مستودع الparts حيث تم تهيئة هذا الContainer مع DirectoryCatalog من اجل الحصول على الparts من المجلد الذي يؤشر عليه الcatalog .
الميثود ComposeParts() هي عبارة عن extension method تقوم بتوسعة الكلاس CompositionContainer وهي معرفة في كلاس AttributedModelServices وتحتاج هذه الميثود إلى الparts المزودة مع الImport attribute ممررة مع الباراميترز . ولأن كلاس Program يمتلك Import attribute مع الproperty المسماة Calculator فيمكن تمرير instance من كلاس Program لهذه الميثود . وعن طريق الimplementation للimports يتم ايجاد الexports ومقابلتها وربطها . وبعد النجاح في استدعاء هذه الميثود يتم ربط الexports مع الimports التي يمكن استعمالهم . اما في حال لم تكن جميع الimports قابلة للربط والمقابلة مع الexports فيتم انشاء exception من نوع ChangeRejectedException والذي قمنا بمعالجته لعرض رسالة الخطأ والخروج من ميثود Run() .
static void Main()
{
var p = new Program();
p.Run();
}
public void Run()
{
var catalog = new DirectoryCatalog(Settings.Default.AddInDirectory);
var container = new CompositionContainer(catalog);
try
{
container.ComposeParts(this);
}
catch (ChangeRejectedException ex)
{
Console.WriteLine(ex.Message);
return;
}يمكننا استعمال الmethods من الإنترفيس ICalculator مع الproperty : Calculator . حيث تستدعي ميثود GetOperation() الmethods للadd-ins التي انشأناها قبل قليل والتي ترجع لنا اربع عمليات حسابية .
يتم استدعاء ميثود Operate() بعد أن نسأل المستخدم أي العمليات تريد استدعاءها ونطلب منه ارقام تلك العملية (الأطراف الحسابية) .
var operations=Calculator.GetOperations();
var operationsDict = new SortedList <string, IOperation >();
foreach (var item in operations)
{
Console.WriteLine("Name: {0}, number operands: {1}", item.Name,
item.NumberOperands);
operationsDict.Add(item.Name, item);
}
Console.WriteLine();
string selectedOp = null;
do
{
try
{
Console.Write("Operation? ");
selectedOp = Console.ReadLine();
if (selectedOp.ToLower() == "exit" ||
!operationsDict.ContainsKey(selectedOp))
continue;
var operation=operationsDict[selectedOp];
double[] operands = new double[operation.NumberOperands];
for (int i=0; i < operation.NumberOperands; i++)
{
Console.Write("\t operand {0}? ", i + 1);
string selectedOperand = Console.ReadLine();
operands = double.Parse(selectedOperand);
}
Console.WriteLine("calling calculator");
double result=Calculator.Operate(operation, operands);
Console.WriteLine("result: {0}", result);
}
catch (FormatException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine();
continue;
}
} while (selectedOp != "exit");
}
}
}ناتج تنفيذ هذا التطبيق البسيط :
Name: +, number operands: 2 Name: -, number operands: 2 Name: /, number operands: 2 Name: *, number operands: 2 Operation? + operand 1? 3 operand 2? 5 calling calculator result: 8 Operation? - operand 1? 7 operand 2? 2 calling calculator result: 5 Operation? exit
يمكننا استعمال مكتبة اضافات add-ins مختلفة كلياً دون الحاجة لاعادة ترجمة التطبيق المستضيف .
تحتوي الأسمبلي AdvCalculator على implementation مختلف لكلاس Calculator لتقديم عمليات حسابية اكثر . يمكننا استخدام هذه الحاسبة مكان الاخرى عن طريق نسخ الأسمبلي إلى المجلد المحدد من طرف DirectoryCatalog في التطبيق المستضيف.
//انظر للملف AdvCalculator/Calculator.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
namespace Bukhari.MEF
{
[Export(typeof(ICalculator))]
public class Calculator : ICalculator
{
public IList <IOperation > GetOperations()
{
return new List <IOperation >()
{
new Operation {]"+", NumberOperands=2},
new Operation {]"-", NumberOperands=2},
new Operation {]"/", NumberOperands=2},
new Operation {]"*", NumberOperands=2},
new Operation {]"%", NumberOperands=2},
new Operation {]"++", NumberOperands=1},
new Operation {]"--", NumberOperands=1}
};
}
public double Operate(IOperation operation, double[] operands)
{
double result=0;
switch (operation.Name)
{
case "+":
result=operands[0] + operands[1];
break;
case "-":
result=operands[0] - operands[1];
break;
case "/":
result=operands[0] / operands[1];
break;
case "*":
result=operands[0] * operands[1];
break;
case "%":
result=operands[0] % operands[1];
break;
case "++":
result=++operands[0];
break;
case "--":
result=--operands[0];
break;
default:
throw new InvalidOperationException(
String.Format("invalid operation {0}", operation.Name));
}
return result;
}
}
}لقد رأينا حتى الان كيفية استخدام الimports والexports والCatalogs ضمن تركيب وبناء MEF وسننتقل بعد قليل لتفاصيل
اكثر واستخدام اخر لMEF ضمن تطبيق WPF من اجل عمل استضافة للاضافات add-ins.
Contracts
يقوم التطبيق الذي سنراه بعد قليل بعمل توسعة للتطبيق السابق . لدينا الان تطبيق WPF يقوم بتحميل اضافات الحاسبة (add-ins) من اجل اضافة المهام الحسابية ويقوم ايضا يتحميل اضافات اخرى تقوم بجلب الإنترفيس الخاص فيها للتطبيق المستضيف.
سنقوم بتعريف نفس الcontracts التي قمنا بتعريفها منذ قليل ICalculator و IOperation وذلك من اجل العمليات الحسابية .
وسنضيف ايضاً contract اخر وهو ICalculatorExtension وهذا الإنترفيس يعرف اثنتين من الproperties وهما Title و Description والذين يمكن استعمالهم من قبل التطبيق المستضيف .
تقوم الميثود GetUI() بارجاع عنصر FrameworkElement يسمح للاضافة بأن ترجع أي عنصر WPF مشتق من FrameworkElement حتى يتم عرضه بالإنترفيس الخاص فيه داخل التطبيق المستضيف الذي هو الحاسبة.
//انظر للملف CalculatorContract/ICalculatorExtension.cs
using System.Windows;
namespace Bukhari.MEF
{
public interface ICalculatorExtension
{
string Title { get; }
string Description { get; }
FrameworkElement GetUI();
}
}تقوم الinterfaces في .NET بصنع contract جديد بين التطبيق المستضيف والإضافة add-in . في حال كان الإنترفيس معرف في اسمبلي منفصلة كما هو الحال مع السمبلي CalculatorContract فلا يمتلك التطبيق المستضيف والadd-in تبعية مباشرة لكن بدلاً عن ذلك يقومون بعمل رفرنس لاسمبلي الcontract .
حسب وجهة نظر MEF فانترفيس الcontract ليس مطلوباً دائماً فالcontract يمكن أن يكون string بسيط . ومن اجل تجنب التعارضات مع contracts اخرى فاسم الstring يجب أن يحتوي على اسم الnamespace مثل Bukhari.MEF.SampleContract كما هو مبين في الكود التالي . ولذلك يتم عمل export لكلاس Foo عن طريق الattribute المسماة Export بstring يمرر لها بدلاً من الإنترفيس .
"Bukhari.MEF.SampleContract")]
public class Foo
{
public string Bar()
{
return "Foo.Bar";
}
}المشكلة باستعمال الcontract كstring هي أن الmethods والproperties والevents المقدمة من قبل النوع ليسو معرفين بشكل قوي .فاما أن يحتاج المستدعي لعمل رفرنس إلى نوع Foo حتى يستعمله أو يستعمل .NET Reflection حتى يصل لاعضاءه .
تجعل الكلمة المحجوزة dynamic في C# 4.0 من استعمال الreflection امراً سهلاً مما يفيدنا بشكل كبير جداً في عدة سيناريوهات . فيمكن للتطبيق المستضيف أن يستعمل نوع dynamic لعمل import للcontract الذي اسمه Bukhari.MEF.SampleContract .
"Bukhari.MEF.SampleContract")]
public dynamic Foo { get; set; }يمكن للproperty : Foo أن تستعمل للوصول بشكل مباشر للميثود Bar() عن طريق استعمال الكلمة المحجوزة dynamic. حيث يتم استدعاء هذا الميثود في مرحلة الruntime .
string s=Foo.Bar();
ملاحظة : تم شرح نوع dynamic في مقالة البرمجة الديناميكية في C# . يمكنك الإطلاع عليها من الرابط التالي:
http://www.el-bukhari.com/2010/10/4_18.html
يمكن أن تكون اسماء الContracts والinterfaces ايضاً في ارتباط بحيث أنه يمكن استعمال الcontract فقط في حال كان اسم الإنترفيس والcontract نفس الشيئ مما يعطينا قابلية استعمال نفس الإنترفيس لعدة contracts.
الصادرات Exports
لقد رأينا في المثال السابق أن الpart المسمى SimpleCalculator والذي يعمل export لنوع Calculator بجميع الmethods والproperties التي له .
يحتوي المثال التالي على SimpleCalculator ايضاً بنفس الimplementation الذي رأيناه سابقاً .
استعملنا هنا ايضاً part اخر وهو WPF User Control library اسمها TemperatureConversion والتي تعرف user interface كما هو مبين في الصورة التالية :

يقدم هذا الكنترول عمليات تحويل بين مقاييس درجات الحرارة (سلسيوس وفهرنهايت وكيلفن) حيث أن الcombo box الأول والثاني هما مصدر التحويل والمربعات النصية تمثل الكمية التي نريد تحويلها ويقوم زر "احسب" ببدء الحسابات الازمة لهذا التحويل .
يمتلك هذا الكنترول implementation بسيط لتحويل درجات الحرارة حيث يعرف الenumeration المسمى TempConversionType عدة تحويلات مختلفة يمكن عملها لهذا الكنترول , ويتم عرض اسماء هذه التحويلات داخل الcombo boxes عن طريق عمل set للproperty المسماة DataContext للكنترول في الconstructor.
تقوم الميثود ToCelsiusFrom() بتحويل الباراميتر t من قيمته الأصلية إلى سلسيوس , ويتم تعريف نوع درجة الحرارة الأصلي في الباراميتر الثانية TempConversionType. وتقوم الميثود FromCelsiusTo() بتحويل قيمة سلسيوس إلى مقياس درجة الحرارة المختار .
نقوم الميثود OnCalculate بمعالجة الevent : Button.Click وتستدعي الmethods : FromCelsiusTo() و ToCelsiusFrom() حتى يتم عمل التحويل حسن النوع الذي اختاره المستخدم .
//انظر للملف TemperatureConversion/TemperatureConversion.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
namespace Bukhari.MEF
{
public enum TempConversionType
{
Celsius,
Fahrenheit,
Kelvin
}
/// <summary >
/// Interaction logic for TemperatureConversion.xaml
/// </summary >
public partial class TemperatureConversion : UserControl
{
public TemperatureConversion()
{
InitializeComponent();
this.DataContext=Enum.GetNames(typeof(TempConversionType));
}
private double ToCelsiusFrom(double t, TempConversionType conv)
{
switch (conv)
{
case TempConversionType.Celsius:
return t;
case TempConversionType.Fahrenheit:
return (t - 32) / 1.8;
case TempConversionType.Kelvin:
return (t - 273.15);
default:
throw new ArgumentException("invalid enumeration value");
}
}
private double FromCelsiusTo(double t, TempConversionType conv)
{
switch (conv)
{
case TempConversionType.Celsius:
return t;
case TempConversionType.Fahrenheit:
return (t * 1.8) + 32;
case TempConversionType.Kelvin:
return t + 273.15;
default:
throw new ArgumentException("invalid enumeration value");
}
}
private void OnCalculate(object sender, System.Windows.RoutedEventArgs e)
{
try
{
TempConversionType from;
TempConversionType to;
if (Enum.TryParse <TempConversionType >((string)comboFrom.SelectedValue, out from) &&
Enum.TryParse <TempConversionType >((string)comboTo.SelectedValue, out to))
{
double result=FromCelsiusTo(ToCelsiusFrom(double.Parse(textInput.Text), from), to);
textOutput.Text=result.ToString();
}
}
catch (FormatException ex)
{
MessageBox.Show(ex.Message);
}
}
}
}هذا الكنترول لحد الان هو مجرد WPF user control بسيط وحتى ننشأ MEF Part نعمل Export للكلاس TemperatureCalculateExtension عن طريق استعمال الattribute المسمى Export . ثم يقوم الكلاس بعمل implement للانترفيس ICalculatorExtension ويضع معلومات لTitle و Description . ويتم ارجاع الكنترول TemperatureConversion من ميثود GetUI() .
//انظر للملف TemperatureConversion/TemperatureCalculatorExtension.cs
using System.ComponentModel.Composition;
using System.Windows;
namespace Bukhari.MEF
{
[Export(typeof(ICalculatorExtension))]
public class TemperatureCalculatorExtension : ICalculatorExtension
{
public string Title
{
get { return "تحويلات درجات الحرارة"; }
}
public string Description
{
get { return "تحويل درجات الحرارة بين سلسيوس وفهرنهايت وكيلفين"; }
}
public FrameworkElement GetUI()
{
return new TemperatureConversion();
}
}
}هناك user controls اخرين يعملون implement للانترفيس ICalculatorExtension وهم تحويلات الأطوال والزمن وهما مشابهان تقريباً لكنترول TemperatureConversion .
قبل أن نكمل مثال حاسبة WPF حتى نعمل import للuser control دعنا نلقي نظرة على الخيارات الاخرى المتاحة مع export . فيمكنك عمل export ل properties و methods وليس فقط انواع كاملة , ويمكنك ايضا اضافة معلومات metadata إلى الexport.
عمل export للproperties والmethods
من الممكن عمل export لproperties لوحدها فقط أو methods لوحدها بدلاً من عمل export لكلاسات كاملة مع جميع الproperties و الmethods والevents .
يمكننا أن نستفيد من عمل export لproperties من استعمال كلاسات لا يمكنك تصديرها جميعاً عن طريق اضافة الattribute : Export إليها ( مثل الكلاسات في اطار عمل .NET أو مكتبات الthird-party). لذلك يتوجب عليك فقط تعريف property لهذا النوع المصدر ومن ثم عمل export لها .
اما عمل export للmethods فيعطينا سيطرة ادق اكثر من عمل export للنوع كاملاً . فالمستدعي لا يحتاج لمعرفة كل معلومات ذلك النوع .
يتم عمل export للmethods بمساعدة الdelegates .
يعرف الكود التالي الmethods : Add() و Subtract() مع الexports . ونوع الexport هو الdelegate ذو النوع Func <double,double,double > والذي هو delegate يقبل باراميترين من نوع double ويرجع نوع double .
أما الmethods التي لا ترجع شيئ فيمكننا استعمال الdelegate ذو النوع Action <T > .
//انظر للملف Operations/Operations.cs
using System;
using System.ComponentModel.Composition;
namespace Bukhari.MEF
{
public class Operations
{
[Export("Add", typeof(Func < double, double, double > ))]
public double Add(double x, double y)
{
return x + y;
}
[Export("Subtract", typeof(Func < double, double, double > ))]
public double Subtract(double x, double y)
{
return x - y;
}
}
}يتم عمل import للmethods (التي قمنا بتصديرها) من قبل الadd-in المسماة SimpleCalculator . ويمكن الparts نفسه أن يستعمل parts اخرين .
يتم تعريف الdelegate مع الattribute المسماة Import حتى نستعمل الextension methods , ويحتوي هذا الattribute على نفس الإسم ةنفس نوع الdelegate في الexport .
ملاحظة : SimpleCalculator هي بذاتها part تعمل export للانترفيس ICalculator وتتكون من parts معمول لهم import.
//انظر للملف SimpleCalculator/Calculator.cs
[Export(typeof(ICalculator))]
public class Calculator : ICalculator
{
[Import("Add", typeof(Func < double, double, double > ))]
public Func < double, double, double > Add { get; set; }
[Import("Subtract", typeof(Func <double, double, double >))]
public Func <double, double, double > Subtract { get; set; }يتم استدعاء الmethods التي عملنا لها import (والممثلة من قبل الdelegates : ِAdd و Subtract ) في الميثود Operate() .
//انظر للملف SimpleCalculator/Calculator.cs
public double Operate(IOperation operation, double[] operands)
{
double result=0;
switch (operation.Name)
{
case "+":
result=Add(operands[0], operands[1]);
break;
case "-":
result=Subtract(operands[0], operands[1]);
break;
case "/":
result=operands[0] / operands[1];
break;
case "*":
result=operands[0] * operands[1];
break;
default:
throw new InvalidOperationException(
String.Format("invalid operation {0}", operation.Name));
}
return result;
}عمل Export للMetadata
يمكننا مع الexports ارفاق معلومات metadata والتي تسمح لنا باضافة معلومات اخرى اضافة للname والtype . وهذا يمكن استعماله في اختيار أي الexports التي تريد استعمالها من جانب الimports .
تم تعديل الميثود Add() (المعمول لها export ) من اجل اضافة ميزة السرعة speed مع الattributes : ExportMetadata
//انظر للملف Operations/Operation.cs
[Export("Add", typeof(Func <double, double, double >))]
[ExportMetadata("speed", "fast")]
public double Add(double x, double y)
{
return x + y;
}قم بعمل implement لميثود اخرى بميزة سرعة مختلفة لكن بنفس نوع الdelegate ونفس الإسم وذلك حتى تحصل على خيار لاختيار implementation اخر لميثود Add() .
//انظر للملف Operations/Operation2.cs
public class Operations2
{
[Export("Add", typeof(Func <double, double, double >))]
[ExportMetadata("speed", "slow")]
public double Add(double x, double y)
{
Thread.Sleep(3000);
return x + y;
}
}اصبح لدينا الان اكثر من ميثود مصدرة لAdd() لذلك يجب تغيير تعريف الImport حيث انه يجب استخدام الattribute المسمى ImportMany في حال كان عندنا اكثر من export واحد بنفس الإسم والنوع.
يطبق هذا الattribute إلى array أو انترفيس IEnumeration <t > . (سوف نناقش ImportMany بتفاصيل اكثر في القسم القادم )
يمكننا الوصول إلى الmetadata عن طريق استعمال array من Lazy <T, TMetadata > حيث أن كلاس Lazy <T > هو جديد في .NET 4 ويستعمل لدعم ما يعرف بالتهيئة البطيئة للانواع عند استعمالها لاول مرة .
Lazy <T, TMetadata > هو مشتق من Lazy <T > ويدعم الوصول (اضافة للbase class) لمعلومات الmetadata عن طريق الproperty : Metadata .
تم التأشير في مثالنا على الميثود عن طريق الdelegate ذو النوع Func <double,double,double > والذي يمثل الباراميتر الأول ل Lazy <T, TMetadata >اما الباراميتر الثاني فهو IDictionary <string,object > من اجل مجموعة الmetadata .
يمكن استعمال الattribute المسمى ExportMetadata عدة مرات من اجل اضافة اكثر من معلومة واحدة تمتلك دائماً key من نوع string وقيمة من نوع object .
//انظر للملف SimpleCalculator/Calculator.cs
[ImportMany("Add", typeof(Func <double, double, double >))]
public Lazy <Func <double, double, double >, IDictionary <string, object > >[]
AddMethods { get; set; }
//[Import("Add", typeof(Func <double, double, double >))]
//public Func <double, double, double > Add { get; set; }تغير الان استدعاء الميثود Add من اجل اللف على عناصر المجموعة
double,double, double >, IDictionary <string, object > >
يتم فحص الkey للمعلومة عن طريق الproperty المسماة Metadata فإذا كانت معلومة السرعة تمتلك القيمة fast فيتم استدعاء العملية عن طريق الproperty : Value للLazy <t > من اجل الوصول إلى الdelegates.
case "+":
// result=Add(operands[0], operands[1]);
foreach (var addMethod in AddMethods)
{
if (addMethod.Metadata.ContainsKey("speed") &&
(string)addMethod.Metadata["speed] == "fast")
result=addMethod.Value(operands[0], operands[1]);
}
// result=operands[0] + operands[1];
break;يمكننا انشاء كلاس export attribute مخصص بدلاً من استعمال الattribute المسمى ExportMetadata حيث يقوم ذلك الكلاس بالإشتقاق من ExportAttribute .
يعرف كلاس SpeedExportAttribute الproperty الإضافية Speed والتي هي من نوع Speed .
//انظر للملف CalculatorUtils/ExportAttribute.cs
using System;
using System.ComponentModel.Composition;
namespace Bukhari.MEF
{
public enum Speed
{
Fast,
Slow
}
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class SpeedExportAttribute : ExportAttribute
{
public SpeedExportAttribute(string contractName, Type contractType)
: base(contractName, contractType) { }
public Speed Speed { get; set; }
}
}ملاحظة : حتى تتقن كيفية عمل attribute خاص يجب عليك أولاً اتقان الReflection.
يمكنك الان استعمال الattribute المسماة SpeedExport مع الميثود المصدرة Add() بدلاً من الattributes السابقين Export و ExportMetadata.
//انظر للملف Operations/Operations.cs
[SpeedExport("Add", typeof(Func < double, double, double > ), Speed=Speed.Fast)]
public double Add(double x, double y)
{
return x + y;
}اما للimport فنحتاج لانترفيس يوجد فيه جميع الmetadata وهذا يجعل بالإمكان الوصول لامكانيات strong typed بالدوت نت.
المعلومة الوحيدة المعرفة من قبل الattribute المسماة SpeedExport هي فقط السرعة Speed . لذلك يعرف الإنترفيس ISpeedCapability الproperty : Speed عن طريق استعمال نفس الenumeration المسمى Speed والذي استعملناه مع الattribute المسمى SpeedExport.
//انظر للملف CalculatorUtils/ISpeedCapabilities.cs
namespace Bukhari.MEF
{
public interface ISpeedCapabilities
{
Speed Speed { get; }
}
}يمكننا الان تغيير تعريف الimport عن طريق استعمال الإنترفيس ISpeedCapability بدلاً من الdictionary التي عرفناها سابقاً.
//انظر للملف SimpleCalculator/Calculator.cs
[ImportMany("Add", typeof(Func <double, double, double > ))]
public Lazy <Func <double, double, double > , ISpeedCapabilities > []
AddMethods { get; set; }يمكننا الان استعمال الproperty المسماة Speed للانترفيس ISpeedCapability مع الimports بشكل مباشر.
foreach (var addMethod in AddMethods)
{
if ( addMethod.Metadata.Speed == Speed.Fast )
result=addMethod.Value(operands[0], operands[1]);
}المستوردات Imports
دعونا الان نذهب لكيفية استعمال WPF User Controls تطبيق الحاسبة المستضيف والذي هو WPF .
شكل التطبيق المستضيف موضوع في الصورة التالية :

تطبيق MEFCalculator هو عبارة عن تطبيق WPF يقوم بتحميل اضافات الحاسبة (التي تعمل implement للانترفيس ICalculator والإنترفيس IOperation) والadd-ins مع user interfaces التي تعمل implement للانترفيس ICalculatorExtension , فحتى نقوم بعمل كونكت مع الexports فإننا بحاجة للimports حتى يتم ربط الimports مع الexports .
عند استعمال الexported parts فإننا بحاجة لimport حتى نعمل connection . يمكن للattribute المسماة Import من عمل كونكت لexport واحد . اما في حال كان هناك اكثر من add-ins واحدة يجب تحميلها فإننا بحاجة لاستعمال الattribute المسمى ImportMany ونحتاج لتعريفها كarray او IEnumerable <T > . وذلك لأن تطبيق الحاسبة المستضيف يسمح باضافة عدة extensions للحاسبة بشرط أن تعمل implement للإنترفيس ICalculatorExtension حتى يتم تحميلها .
يعرف الكلاس CalculatorExtensionImport الproperty المسماة CalculatorExtension من نوع IEnumerable <ICalculatorExtension > من اجل الوصول لجميع الextension parts في الحاسبة .
//انظر للملف MEFCalculator/CalculatorExtensionImport.cs
using System.Collections.Generic;
using System.ComponentModel.Composition;
namespace Bukhari.MEF
{
public class CalculatorExtensionImport
{
[ImportMany(AllowRecomposition=true)]
public IEnumerable <ICalculatorExtension > CalculatorExtensions { get; set; }
}
}تسمح Import و ImportMany باستعمال ContractName و ContractType من اجل مقابلة وربط الimport لexport.
هناك properties اخرى يمكن وضعها مع هذه الattributes وهم AllowRecomposition و RequiredCreationPolicy . حيث أن الاولى تسمح بعمل ربط ديناميكي لل exports الجدد عندما يكون التطبيق يعمل وايضاً عند ازالة الexport.
اما مع RequiredCreationPolicy فيمكنك اختيار اذا ما الparts يجب مشاركتها بين المستخدمين (CreationPolicy.Shared) أو يجب عدم مشاركتها (CreationPolicy.NotShared) أو بامكاننا ترك هذا الأمر للcontainer ليحدده عن طريق وضع (CreationPolicy.Any) .
يمكن عمل implement للانترفيس IPartImportsSatisfiedNotification من أجل التأكد من أن جميع الimports تم استيرادهم بنجاح . وهذا الإنترفيس يحتوي على ميثود واحدة فقط وهي OnImportsSatisfied() والتي تستدعى عندما يتم استيراد الimports للكلاس بنجاح . حيث أن الميثود في كلاس CalculatorImport تطلق الevent المسمى ImportsSatisfied .
//انظر للملف MEFCalculator/CalculatorImport.cs
using System;
using System.ComponentModel.Composition;
using System.Windows.Controls;
namespace Bukhari.MEF
{
public class CalculatorImport : IPartImportsSatisfiedNotification
{
public event EventHandler <ImportEventArgs > ImportsSatisfied;
[Import(typeof(ICalculator))]
public ICalculator Calculator { get; set; }
public void OnImportsSatisfied()
{
if (ImportsSatisfied != null)
ImportsSatisfied(this, new ImportEventArgs {
StatusMessage = "ICalculator import successful" });
}
}
}يتم ربط الevent للCalculatorImport مع event handler عند انشاء CalculatorImport من اجل كتابة رسالة للTextBlock حتى نعرض معلومات الstatus.
//انظر للملف MEFCalculator/MainWindow.xaml.cs
private void InitializeContainer()
{
var catalog = new DirectoryCatalog(
Properties.Settings.Default.AddInDirectory);
container = new CompositionContainer(catalog);
calcImport = new CalculatorImport();
calcImport.ImportsSatisfied += (sender, e) = >
{
textStatus.Text += String.Format("{0}\n", e.StatusMessage);
};
container.ComposeParts(calcImport);
InitializeOperations();
}التحميل المتأخر Lazy Loading للparts
يتم تحميل الparts مع الcontainer بشكل تلقائي . وذلك عن طريق استدعاء الextension method المسماة ComposeParts في CompositionContainer , ومع مساعدة كلاس Lazy <T > فيمكن للparts أن تحمل عند الوصول الأول first access .
يسمح نوع Lazy<T> بعمل instance متاخر من النوع T ويعرف الproperties : IsValueCreated و Value حيث ان الأولى هي Boolean ترجع معلومات عن النوع T اذا ما تم عمل instance له أم لا أما Value فتهيئ النوع T عند الوصول الأول له وترجع instance.
يمكن التصريح عن الimport للadd-in لتكون من نوع Lazy <T > كما هو مبين في مثال Lazy <ICalculator > .
//انظر للملف MEFCalculator/CalculatorImport.cs
[Import(typeof(ICalculator))]
public Lazy <ICalculator > Calculator { get; set; }يحتاج استدعاء الproperty المعمول لها implement لبعض التعديلات ايضاً من اجل الوصول للproperty المسماة Value لنوع Lazy <T > . calcImport هو متغير من نوع CalculatorImport . ترجع الproperty المسماة Calculator نوع Lazy <ICalculator > وتقوم الproperty المسماة Value بعمل instance من النوع المستورد عند الوصول الأول وترجع الإنترفيس ICalculator .
يمكن الان استدعاء الميثود GetOpertation() للحصول على جميع العمليات الحسابية من الadd-in للحاسبة.
//انظر للملف MEFCalculator/MainWindow.xaml.cs
private void InitializeOperations()
{
Contract.Requires(calcImport != null);
Contract.Requires(calcImport.Calculator != null);
var operators=calcImport.Calculator.Value.GetOperations();
foreach (var op in operators)
{
var b = new Button();
b.Width=40;
b.Height=30;
b.Content=op.Name;
b.Margin = new Thickness(2);
b.Padding = new Thickness(4);
b.Tag=op;
b.Click += new RoutedEventHandler(DefineOperation);
listOperators.Items.Add(b);
}
}الContainers والExport Providers
تتم عملية الimport للparts بمساعدة Container معين وانواع هذه الcontainer موجودة داخل الnamespace : System.ComponentModel.Composition.Hosting .
كلاس CompositionContainer هو الcontainer للparts . يمكن وضع عدة objects من الExport Providers داخل الconstructor لهذا الكلاس بالإضافة ComposablePartCatalog (سنناقش الCatalogs في القسم التالي).
يسمح الExport Provider لنا بالوصول لجميع الexports بشكل برمجي مع الoverload methods المسماة GetExport <T >() .
يتم استعمال الexport provider للوصول للCatalog مع العلم أن الCompositionContainer هو بنفسه export provider . وهذا يجعل بالإمكان عمل containers داخل containers اخرى .
يتم تحميل الparts عند استدعاء الميثود Compose() (في حال لم يكن التحميل متأخر Lazy). وحتى الان قمنا باستعمال الميثود ComposePart() كما هو مبين في الميثود InitializeContainer() .
//انظر للملف MEFCalculator/MainWindow.xaml.cs
private void InitializeContainer()
{
catalog = new DirectoryCatalog(
Properties.Settings.Default.AddInDirectory);
container = new CompositionContainer(catalog);
calcImport = new CalculatorImport();
calcImport.ImportsSatisfied += (sender, e) = >
{
textStatus.Text += String.Format("{0}\n", e.StatusMessage);
};
container.ComposeParts(calcImport);
InitializeOperations();
}الميثود ComposePart() هي عبارة عن extension method معرفة في الكلاس AttributeModelServices ويقدم هذا الكلاس عدة methods تستعمل الattributes وال.NET Reflection من اجل الوصول لمعلومات الpart ولاضافة parts إلى الContainer .
يمكننا استعمال الميثود Compose() التابعة لCompositionContainer بدلاً من استعمال الextension method السابقة .
تعمل ميثود Compose() مع كلاس CompositionBatch والذي يمكن استعماله لتعريف أي الparts التي يجب اضافتها وايها يجب ازالتها من الcontainer.
تمتلك الmethods : AddPart() و RemovePart() عدة overloads حيث يمكن اضافة الattributed part أو الpart المشتق من الbase class : ComposablePart .
(calcImport هو instance من كلاس CalculatorImport ويحتوي عالImport attributes).
var batch = new CompositionBatch(); batch.AddPart(calcImport); container.Compose(batch);
يحمل التطبيق المستضيف MEFCalculator نوعين مختلفين من الparts تقوم الأولى بعمل implement للانترفيس ICalculator ويتم تحميلها في مثيود InitializeContainer الموضحة سابقاً. بينما الpart التي تعمل implement للانترفيس ICalculatorExtension يتم تحميلها في ميثود RefreshExtensions .
تحضر Container.ComposeParts الimport من كلاس CalculatorExtensionImport وتبرطه مع جميع الexports المتوفرة من قبل الcatalog. ولأن هناك عدة add-ins يتم تحميلهم ديناميكياً (يمكن للمستخدم اختيار الadd-in التي يريد من اجل استعمالها) فيتم توسيع الكنترول Menu بشكل ديناميكي ويقوم التطبيق باللف على جميع الإضافات للحاسبة ويتم انشاء MenuItem لكل منها .
يحتوي الهيدر لكنترول MenuItem على Lable مع الproperty : Title للإضافة وايضاً Checkbox سنستعمله لاحقاً لازالة الإضافة بشكل ديناميكي .
تم وضع الproperty : ToolTip بقيمة الproperty : Description لكنترول الextender (الإضافة) . تم ايضا وضع الevent : Click للMenuItem إلى الميثود ShowAddIn() وهذه الميثود تستدعي ميثود GetUI() للextender control وذلك من اجل عرض الكنترول داخل TabItem جديد من الTabControl .
private void RefreshExensions()
{
catalog.Refresh();
calcExtensionImport = new CalculatorExtensionImport();
calcExtensionImport.ImportsSatisfied += (sender, e) = >
{
this.textStatus.Text += String.Format("{0}\n", e.StatusMessage);
};
container.ComposeParts(calcExtensionImport);
menuAddins.Items.Clear();
foreach (var extension in calcExtensionImport.CalculatorExtensions)
{
var menuItemHeader = new StackPanel { Orientation=Orientation.Horizontal };
menuItemHeader.Children.Add(new Label { Content=extension.Title });
var menuCheck = new CheckBox { IsChecked = true };
menuItemHeader.Children.Add(menuCheck);
var menuItem = new MenuItem {
Header=menuItemHeader,
ToolTip=extension.Description,
Tag=extension
};
menuCheck.Tag=menuItem;
menuItem.Click += ShowAddIn;
menuAddins.Items.Add(menuItem);
}
}
private void ShowAddIn(object sender, RoutedEventArgs e)
{
var mi=e.Source as MenuItem;
var ext=mi.Tag as ICalculatorExtension;
FrameworkElement uiControl=ext.GetUI();
var headerPanel = new StackPanel { Orientation=Orientation.Horizontal };
headerPanel.Children.Add(new Label { Content=ext.Title });
var closeButton = new Button { Content = "X" };
var ti = new TabItem { Header=headerPanel, Content=uiControl };
closeButton.Click += delegate
{
tabExtensions.Items.Remove(ti);
};
headerPanel.Children.Add(closeButton);
tabExtensions.SelectedIndex=tabExtensions.Items.Add(ti);
}حتى نزيل الexports بشكل ديناميكي فإننا نضع الevent : Unchecked للكنترول CheckBox االموجود ضمن الهيدر لMenuItem .
يمكن الوصول لICalculatorExtension من الproperty : Tag للكنترول MenuItem وذلك لأننا وضعنا set لها في الكود السابق. وحتى نزيل الpart من الcontainer يجب أولاً انشاء object من نوع CompositeBatch والذي سيحتوي على الparts التي سنزيلها.
تحتاج ميثود RemovePart() لكلاس CompositeBatch لobject من نوع ComposablePart حيث أنه لا يوجد overload لها للattributed objects .
يمكن انشاء ComposablePart من attribute object بمساعدة كلاس AttributeModleServices وباستعمال الميثود CreatePart() وبعدها يتم تمرير CompositionBatch إلى ميثود Compose() من الCompositionContainer لازالة الpart منه .
as CheckBox).Tag as MenuItem; ICalculatorExtension ext=mi.Tag as ICalculatorExtension; ComposablePart part = AttributedModelServices.CreatePart(ext); var batch = new CompositionBatch(); batch.RemovePart(part); container.Compose(batch); MenuItem parentMenu=mi.Parent as MenuItem; parentMenu.Items.Remove(mi); };
ملاحظة : يمكن ازالة الpart فقط في حال كانت الproperty المسماة AllowRecomposition للImport attribute موضوع لtrue.
يمكنك الحصول على معلومات حول الexports المضافة والمحذوفة مع الexport provider عن طريق عمل implement لhandler للevent : ExportChanged . يحتوي باراميتر e لنوع ExportsChangedEventArgs على قائمة الexports المضافة والمحذوفة والمطبوعة للكنترول TextBlock.
var container = new CompositionContainer(catalog);
container.ExportsChanged += (sender, e)= >
{
var sb = new StringBuilder();
foreach (var item in e.AddedExports)
{
sb.AppendFormat("added export {0}\n", item.ContractName);
}
foreach (var item in e.RemovedExports)
{
sb.AppendFormat("removed export {0}\n", item.ContractName);
}
this.textStatus.Text += sb.ToString();
};الفهارس Catalogs
الCatalog هو مكان بحث MEF عن الparts المطلوبة . استعملنا في تطبيقنا في هذه المقالة DirectoryCatalog من اجل تحميل الassemblies مع الparts من مجلد معين . يمكنك باستعمال DirectoryCatalog من الحصول على المعلومات المتغيرة مع الevent : Changed واللف على جميع الإضافات المضافة والمحذوفة .
لا يسجل DirectorCatalog تغييرات نظام الملفات بنفسه لكن يستدعي الميثود Refresh() لDirectoryCatalog وفي حال كان هناك تغييرات تم عملها منذ القراءة الأخيرة فيتم اطلاق الevents : Changed و Changing .
//انظر للملف MEFCalculator/MainWindow.xaml.cs
private void InitializeContainer()
{
catalog = new DirectoryCatalog(Properties.Settings.Default.AddInDirectory);
container = new CompositionContainer(catalog);
catalog.Changed += (sender, e)= >
{
var sb = new StringBuilder();
foreach (var definition in e.AddedDefinitions)
{
foreach (var metadata in definition.Metadata)
{
sb.AppendFormat(
"added definition with metadata - key: {0}, " +
"value: {1}\n", metadata.Key, metadata.Value);
}
}
foreach (var definition in e.RemovedDefinitions)
{
foreach (var metadata in definition.Metadata)
{
sb.AppendFormat(
"removed definition with metadata - key: {0}, " +
"value: {1}\n", metadata.Key, metadata.Value);
}
}
this.textStatus.Text += sb.ToString();
};
//...ملاحظة : يمكن الحصول على اشعار مباشر عندما يتم اضافة add-in إلى المجلد حيث يمكنك استعمال System.IO.FileSystemWatcher من اجل تسجيل التغييرات على مجلد الإضافات add-ins . واستدعاء الميثود Refresh لDirectoryCatalog عن طريق الevent : Changed من ال FileSystemWatcher.
يحتاج CompositionContainer فقط إلى ComposablePartCatalog من اجل ايجاد الparts .
ملاحظة : DirectoyCatalog هو مشتق من ComposablePartCatalog .
انواع الCatalogs
DirectoryCatalog : يبحث عن الparts داخل مجلد ما .
- AssemblyCatalog : يبحث عن الparts بشكل مباشر ضمن اسمبلي موضوعة refrenced , وعلى عكس DirectoryCatalog حيث يمكن هناك للasseblies أن تتغير أثناء وقت التشغيل بينما في الAssemblyCatalog هي ثابتة والparts لا يمكن أن تتغير .
- TypeCatalog : يبحث عن الimports ضمن قائمة انواع حيث يمكن تمرير IEnumerable <Type > إلى الconstructor لهذا الCatalog .
AggregateCatalog : وهو عبارة عن فهرس للcatalogs ويمكن انشاء هذا الcatalog من عدة objects لComposablePartCatalog ويبحث في جميع هذه الcatalogs . فمثلاً يمكنك انشاء AsseblyCatalog للبحث عن الimports ضمن اسمبلي وايضاً انشاء اثنين من الobjects من نوع DirectoryCatalog من اجل البحث في مجلدين مختلفين . فيقوم AggregateCatalog بربط هذه الثلاثة catalogs معاص من اجل عمليات البحث عن الimports.
عند تشغيل التطبيق كما سنرى في الصورة القادمة سيتم تحميل الإضافة SimpleCalculator ويمكنك عمل بعض الحسابات مع العمليات المدعومة من قبل تلك الadd-in . ومن خيار "الإضافات" في الmenu يمكنك بدأ تشغيل الadd-ins التي تعمل implement للانترفيس ICalculatorExtender ورئية الuser interface من كنترول الtab التابع لهذه الadd-in.
المعلومات حول الexports والتغييرات على DirectoryCatalog موضجة في مكان الstatus اسفل البرنامج .
لاحظ أنه يمكنك ازالة الIcalculatorExtender من مجلد الadd-ins عندما يكون التطبيق لا يعمل . وبعدها قم بنسخ الadd-in إلى المجلد عندما يكون التطبيق يعمل واعمل تحديث للاضافات لرؤية الadd-ins الجديدة اثناء وقت التشغيل .

خلاصة المقالة :
تعرفنا في هذه المقالة على التقنية MEF الجديدة في اطار عمل .NET 4 .
يستعمل الimplementation ل MEF الattributes لكي يجد الadd-ins ويعمل كونكت معها . لقد رأينا كيفية انشاء وتحميل الadd-ins ورأينا امكانيات استعمال الinterfaces والmethods مع الexports.
لقد تعلمنا ايضاً كيف يمكن استعمال الcatalogs لايجاد الadd-ins بشكل ديناميكي وتعلمنا كيفية استعمال الcontainers من اجل الربط بين الimports والexports.
سنتكلم في مقالة اخرى ان شاء الله عن تقنية MAF والتي تقدم عزل افضل بين التطبيق المستضيف والadd-in عن طريق ال appdomains والprocesses المنفصلين . لكن هذه التقنية اصعب من MEF وتحتاج لعمل برمجي اكثر .
لمزيد من المعلومات عن تقنية MEF راجع الرابط التالي: