المتابعون للمدونة

الخميس، 18 سبتمبر 2014

Reflection C#



لمبرمجي سي شارب:
استخدام الانعكاس Reflection لتغيير طريقة عمل بعض أدوات دوت نت بدون إعادة كتابة كودها من البداية
(يمكن قراءة نسخة فيجوال بيزيك من هنا)

 

كلنا يعلم أن العناصر الخاصة Private Members المعرّفة داخل أي فئة Class لا يمكن التعامل معها إلا من داخل هذه الفئة.. هذه هي القاعدة العامة، لكن المثير في الأمر أنك تستطيع كسر هذه القاعدة باستخدام تقنية الانعكاس Reflection.

يوجد في إطار العمل .NET Framework نطاق اسمه System.Reflection يحتوي على الفئات اللازمة للتعامل مع ملفات التجميع Assemblies والحصول على معلومات عما تحتويه من فئات وسجلات Structures، وما تحتويه هذه العناصر من خصائص ووسائل.. هذا مفيد في حالات كثيرة (مثلا: عند إنشاء مترجم كود Compiler يعمل على دوت نت، أو عند كتابة كود ينسخ كائنا نسخا عميقا Deep Cloning.. إلخ).

ويمكننا استخدام تقنية الانعكاس للبحث في كائن (اسمه Obj مثلا) عن خاصية لها اسم معين (وليكن Prop1) والحصول على قيمتها بالكود التالي:

Type type = Obj.GetType();

PropertyInfo pr = type.GetProperty("Prop1", BindingFlags.Instance | BindingFlags.NonPublic);

var Value = pr.GetValue(Obj, null);

ويمكن تغيير قيمة الخاصية (مثلا إلى القيمة Test) كالتالي:

pr.SetValue(Obj, "Test", null);

وباستخدام طرق مماثلة يمكن التعامل مع الحقول (المتغيرات المعرفة على مستوى الفئة) كما يمكن استدعاء الوسائل (الإجراءات والدوال).. وحتى أريحك من هذا العناء، وضعت لك الكود الذي يفعل كل هذا في فئة اسمها ReflectionHelper:

 

using System.Reflection;

 

public class ReflectionHelper

{

 

        public static object GetPropertyValue(object Obj, string PropertyName)

        {

            Type type = Obj.GetType();

            PropertyInfo pr = type.GetProperty(PropertyName, BindingFlags.Instance | BindingFlags.NonPublic);

            return pr.GetValue(Obj, null);

        }

 

        public static void SetPropertyValue(object Obj, string PropertyName, object Value)

        {

            Type type = Obj.GetType();

            PropertyInfo pr = type.GetProperty(PropertyName, BindingFlags.Instance | BindingFlags.NonPublic);

            pr.SetValue(Obj, Value, null);

        }

 

        public static object GetFieldValue(object Obj, string FieldName)

        {

            Type type = Obj.GetType();

            FieldInfo Fld = type.GetField(FieldName, BindingFlags.Instance | BindingFlags.NonPublic);

            return Fld.GetValue(Obj);

        }

 

        public static void SetFieldValue(object Obj, string FieldName, object Value)

        {

            Type type = Obj.GetType();

            FieldInfo Fld = type.GetField(FieldName, BindingFlags.Instance | BindingFlags.NonPublic);

            Fld.SetValue(Obj, Value);

        }

 

        public static object ExcuteMethod(object Obj, string MethodName, params object[] Params)

        {

            Type type = Obj.GetType();

            MethodInfo M = type.GetMethod(MethodName, BindingFlags.Instance | BindingFlags.NonPublic);

            return M.Invoke(Obj, Params);

        }

 

}

 

 

هذه الفئة تحتوي على وسائل مشتركة Shared Methods لقراءة وتغيير قيم الخصائص والحقول، ولاستدعاء الإجراءات والدوال.. لاحظ أن الوسيلة ExcuteMethod لها معاملان:

-      المعامل الأول يستقبل اسم الوسيلة التي تريد استدعاها.

-  والمعامل الثاني عبارة عن مصفوفة معاملات Parameter Array ليستقبل أي عدد من المعاملات التي تريد إرسالها إلى الوسيلة.. ولو لم يكن للوسيلة المطلوب استدعاؤها أية معاملات، فلا ترسل أي شيء إلى المعامل الثاني.

 

تعال نجرب هذه الفئة.. سننشئ فئة بسيطة للتجريب، كل عناصرها خاصة، ونرى كيف يمكننا التعامل مع هذه العناصر من خلال الانعكاس:

public class TestClass1

{

 

        private int X;

 

        private int Y { get; set; }

 

        private void Show()

        {

            MessageBox.Show("X = " + X + ", Y= " + Y);

        }

 

        private int Add()

        {

            return X + Y;

        }

}

يمكنك الآن أن تجرب ما يلي في حدث ضغط أي زر:

TestClass1 C = new TestClass1( );

ReflectionHelper.SetFieldValue(C, "X", 1);

ReflectionHelper.SetPropertyValue(C, "Y", 2);

ReflectionHelper.ExcuteMethod(C, "Show");

var R = ReflectionHelper.ExcuteMethod(C, "Add");

MessageBox.Show(R.ToString( ));

هذا الكود يضع القيمة 1 في الحقل الخاص X والقيمة 2 في الخاصية الخاصة Y، ثم يستدعي الإجراء الخاص Show الذي سيعرض الرسالة:

X=1, Y= 2

ثم يستدعي الدالة Add التي ستعيد القيمة 3، ثم يعرضها في رسالة.

تمام!

السؤال الآن: بم يفيدنا هذا التحايل على قوانين البرمجة الكائنية OOP؟

في بعض الأحيان ونحن نتعامل مع أدوات نماذج الويندوز WinForms Controls، نحتاج إلى تغيير أداء بعض الأدوات أو تصحيح خطأ في هذا الأداء، وقد لا نجد وسيلة مباشرة لفعل هذا، سوى بالتعامل مع العناصر الخاصة الموجودة في داخل الأداة وتغيير قيم بعض الخصائص والحقول.. لاحظ أن تقنية نماذج الويندوز خارج التطوير حاليا ولا أمل أن تحل ميكروسوفت أي مشكلة في هذه الأدوات.. لاحظ أيضا أن أدوات WPF مرنة جدا ويمكنك التحكم في كل شيء فيها تقريبا وتغير أشكالها بصورة مدهشة من خلال القوالب Templates وأنماط التنسيق Styles، بحيث لا تحتاج إلى حيلة اللجوء إلى تقنية الانعكاس.. لكن ربما ما زلت تحتاج لهذه التقنية عند التعامل مع بعض فئات إطار العمل الأخرى.

إذن، فقد عرفنا لماذا.. لكن يتبقى سؤال هام لا بد أنه يدور في ذهنك: كيف يمكنني أن أعرف أسماء العناصر الخاصة الموجودة داخل فئات إطار العمل؟

الإجابة ذكرتها سابقا في هذا الموضوع: "هل دوت نت مفتوحة المصدر؟"


كل ما عليك فعله هو استعراض كود الفئة، ورؤية تركيبها الداخلي، وتحديد الخصائص والحقول والوسائل التي يمكنها أن تؤدي الوظيفة التي تريدها، ومن ثم تغير قيمها بتقنية الانعكاس.. وهو أمر يحتاج إلى بعض الخبرة البرمجية، ولكنه سيعطيك قدرات غير محدودة.

 

تعال نأخذ مثالا بسيطا:

ماذا لو أردت إجبار الأدوات الموضوعة على النموذج على إعادة التحقق من صحة محتوياتها Validation، باستدعاء الحدث Validating الخاص بها؟

للأسف لا توجد طريقة مباشرة لفعل هذا، فالأدوات لا تستدعي الحدث Validating إلا عند مغادرتها أو عند محاولة إغلاق النموذج.

وللتحايل على هذا، يمكنك استدعاء الإجراء الخاص OnValidating الخاص بكل أداة موضوعة على النموذج، وهو سيقوم باستدعاء الحدث Validating الخاص بالأداة.. تعال نأخذ مثالا على مربع نص اسمه TextBox1:

var Args = new System.ComponentModel.CancelEventArgs( );

ReflectionHelper.ExcuteMethod(TextBox1, "OnValidating", Args);

if (Args.Cancel)

    MessageBox.Show("Invalid");

جرب الآن تعريف معالج للحدث Validating، واكتب فيه ما يلي:

private void TextBox1_Validating(object sender,

                                System.ComponentModel.CancelEventArgs e)

{

         e.Cancel = (TextBox1.Text == "");

}

وإذا لم تسخدم نلفذة الخصائص لإضافة معالج الحدث، فلا تنس ربط هذا المعالج بالحدث بإضافة الجملة التالية إلى حدث تحميل النموذج مثلا:

TextBox1.Validating += TextBox1_Validating;

لو جربت البرنامج، فستجد أن كود الانعكاس يؤدي إلى استدعاء هذا المعالج.

 

ملحوظة:

يمكنك استدعاء الإجراء TextBox1_Validating مباشرة بدلا من استخدام تقنية الانعكاس.. لكن لو لديك نافذة عليها 20 أداة، وتريد إجبارها جميعا على إعادة التحقق من صحة محتوياتها، ففي هذه الحالة من الأفضل أن تستخدم حلقة تكرار Loop للمرور عبر كل الأدوات الموجودة في الخاصية this.Controls وإجبار كل أداة على إطلاق الحدث Validating باستخدام تقنية الانعكاس.. هذا مختصر جدا مقارنة بكتابة عشرين جملة لاستدعاء عشرين معالج لهذا الحدث في كل أداة!

 

مثال آخر: أنا أستخدم هذا الكود مع الأداة ReportViewer التي تقوم بعرض التقارير، لكي أجبرها على تغيير طريقة العرض Zoom لتكون مناسبة لعرض الشاشة في الوضع الافتراضي:

var ReportToolBar = ReflectionHelper.GetFieldValue(Rv, "reportToolBar");

var CmbZoom = ReflectionHelper.GetFieldValue(ReportToolBar, "zoom");

CmbZoom.SelectedIndex = 0;

حيث Rv هو اسم أداة التقارير الموضوعة على النموذج.

واضح أن الحقل الخاص reportToolBar يشير إلى شريط الأدوات الموضوع على أداة عرض التقارير (والتي لا تسمح لنا بالتحكم فيه بطريقة عادية)، والحقل الخاص zoom يشير إلى القائمة المنسدلة ComboBox الموضوعة على شريط الأدوات، والتي تحتوي على نسب العرض المختلفة.

أردت أيضا أن أجعل الأداة ReportViewer تسمح بحفظ التقرير كملف وورد 2003.. هذا الاختيار موجود داخل الأداة لكن تم إخفاؤه في الإصدارات الأخيرة والاكتفاء بحفظ التقرير في ملف وورد 2007 (وهو يسبب بعض المشاكل الشكلية في عرض التقرير).. لفعل هذا استخدمت هذا الإجراء:

private void EnableRenderExtension(string extensionName, string localizedExtensionName)

{

      foreach (RenderingExtension extension in Rv.LocalReport.ListRenderingExtensions)

      {

          if (extension.Name == extensionName)

          {

               ReflectionHelper.SetFieldValue(extension, "m_isVisible", true);

               ReflectionHelper.SetFieldValue(extension, "m_localizedName", localizedExtensionName);

          }

      }

}

هذا الإجراء يمر عبر كل الامتدادات التي تتعامل معها أداة عرض التقارير، ويستخدم الانعكاس لجعل الامتداد المرسل إليه كمعامل مرئيا وله الاسم الذي تريده.. وقد استدعيت هذا الإجراء في برنامجي كالتالي:

EnableRenderExtension("WORD", "Word 2003 .doc");          EnableRenderExtension("WORDOPENXML", "Word 2007 .docx");

 

 


ليست هناك تعليقات:

إرسال تعليق

صفحة الشاعر