س:
هل يمكنني أن أغير قيمة خاصية غير عامة 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")
ليست هناك تعليقات:
إرسال تعليق
ملحوظة: يمكن لأعضاء المدونة فقط إرسال تعليق.