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

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

Reflection


س: هل يمكنني أن أغير قيمة خاصية غير عامة Private Property موجودة داخل فئة من فئات إطار العمل؟!.. وهل يمكنني تغيير طريقة عمل بعض أدوات دوت نت بدون إعادة كتابة كودها من البداية؟ 

ج: نعم!


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

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

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

Dim type As Type = Obj.GetType( )

Dim pr As PropertyInfo = type.GetProperty("Prop1", BindingFlags.Instance Or BindingFlags.NonPublic)

Dim Value  =   pr.GetValue(Obj, Nothing)

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

pr.SetValue(Obj, "Test", Nothing)

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


 

Imports System.Reflection

 

Public Class ReflectionHelper

 

    Public Shared Function GetPropertyValue(Obj As Object, PropertyName As String) As Object

        Dim type As Type = Obj.GetType()

        Dim pr As PropertyInfo = type.GetProperty(PropertyName, BindingFlags.Instance Or BindingFlags.NonPublic)

        Return pr.GetValue(Obj, Nothing)

    End Function

 

    Public Shared Sub SetPropertyValue(Obj As Object, PropertyName As String, Value As Object)

        Dim type As Type = Obj.GetType()

        Dim pr As PropertyInfo = type.GetProperty(PropertyName, BindingFlags.Instance Or BindingFlags.NonPublic)

        pr.SetValue(Obj, Value, Nothing)

    End Sub

 

    Public Shared Function GetFieldValue(Obj As Object, FieldName As String) As Object

        Dim type As Type = Obj.GetType()

        Dim Fld As FieldInfo = type.GetField(FieldName, BindingFlags.Instance Or BindingFlags.NonPublic)

        Return Fld.GetValue(Obj)

    End Function

 

    Public Shared Sub SetFieldValue(Obj As Object, FieldName As String, Value As Object)

        Dim type As Type = Obj.GetType()

        Dim Fld As FieldInfo = type.GetField(FieldName, BindingFlags.Instance Or BindingFlags.NonPublic)

        Fld.SetValue(Obj, Value)

    End Sub

 

    Public Shared Function ExcuteMethod(Obj As Object, MethodName As String, ParamArray Params() As Object)

        Dim type As Type = Obj.GetType()

        Dim M As MethodInfo = type.GetMethod(MethodName, BindingFlags.Instance Or BindingFlags.NonPublic)

        Return M.Invoke(Obj, Params)

    End Function

 

End Class

 

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

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

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

 

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

Public Class TestClass1

 

    Private X As Integer

 

    Private Property Y As Integer

 

    Private Sub Show()

        MsgBox("X = " & X & ", Y= " & Y)

    End Sub

 

    Private Function Add() As Integer

        Return X + Y

    End Function

End Class

 

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

Dim C As New TestClass1

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

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

ReflectionHelper.ExcuteMethod(C, "Show")

Dim R = ReflectionHelper.ExcuteMethod(C, "Add")

MsgBox(R)

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

X=1, Y= 2

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

تمام!

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

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

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

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


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

 

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

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

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

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

Dim Args As New System.ComponentModel.CancelEventArgs()

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

If Args.Cancel Then MsgBox("Invalid")

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

Private Sub TextBox1_Validating(sender As Object, e As System.ComponentModel.CancelEventArgs) Handles TextBox1.Validating

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

End Sub

ستجد أن الكود السابق يؤدي لاستدعاء هذا المعالج.

 

ملحوظة:

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

 

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

Dim ReportToolBar = ReflectionHelper.GetFieldValue(Rv, "reportToolBar")

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

CmbZoom.SelectedIndex = 0

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

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

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

Private Sub EnableRenderExtension(ByVal extensionName As String, ByVal localizedExtensionName As String)

        For Each extension As RenderingExtension In Rv.LocalReport.ListRenderingExtensions

            If extension.Name = extensionName Then

                ReflectionHelper.SetFieldValue(extension, "m_isVisible", True)

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

            End If

        Next extension

End Sub

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

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

 

 


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

إرسال تعليق

ملحوظة: يمكن لأعضاء المدونة فقط إرسال تعليق.

صفحة الشاعر