• الإعلانات

    • فيصل الحربي

      تسجيل عضوية جديدة في المنتدى   01/31/2016

      السلام عليكم ورحمة الله وبركاته  عزيزي العضو الجديد :  حاليا رسالة الإيميل لتأكيد صحة إيميلكم تذهب للبريد العشوائي ( جاري حل المشكلة )  فإذا لم تجد رسالة التحقق من إيميلكم في صندوق الوارد لديكم إتجه للبريد العشوائي ( JUNK)  وقم بتفعيل إشتراككم من هناك   

ddata11

الخبراء المعتمدون
  • عدد المشاركات

    547
  • تاريخ الانضمام

  • تاريخ اخر زياره

السمعه بالموقع

7 عادي

1 متابع

عن ddata11

  • الرتبة
    خبير دوت نت

طرق الإتصال

  • ICQ 0

معلومات الملف الشخصي

  • الجنس ذكر
  • الدولة : اليمن

أحدث الزائرين لملفلك الشخصي

7,935 زياره للملف الشخصي
  1. تعلم Xaml

    جزاكم الله كل خير اخواني الأفاضل، وكل عام وانتم بخير.
  2. تعلم Xaml

    مقدمة: XAML (اختصار لـ Extensible Application Markup Language وتلفظ "زامل")، وهي لغة وسوم لتمثيل الكائنات في الدوت نت، تم إنشائها خصيصاً من اجل WPF وتستخدم في تقنيات اخرى مثل WF (Workflow Foundation) و Silverlight. في WPF تستخدم XAML اساسا من اجل تعريف واجهات المستخدم والغاية من هذا هو فصل الأجزاء المتعلقة بتعريف الواجهة عن الكود في التطبيق. بحيث يكون بامكان المصممين إنشاء الواجهات باستخدام ادوات مخصصة للتعامل مع الصور والرسومات مثل Microsoft Expression Blend، بينما يقوم المطورين بتعريف منطق وسلوك تلك الواجهات من خلال الكود باستخدام ادوات مثل Visual Studio. تعتمد لغة XAML اساسا على لغة XML ، لذا فإن اي مستند XAML في الواقع هو ملف XML ، ويخضع لنفس القواعد والشروط، مثلها مثل اي لغة وسوم اخرى ، إلا ان XAML اكثر صرامة من غيرها حيث ان كل عنصر فيها يتم تحويله إلى كائن في الدوت نت. وكونها تعتمد على XML ، فإن هذا اضاف سهولة ومرونة كبيرة ، فيمكن لاكثر من اداة ان تتعامل مع نفس المستند في نفس الوقت ، ومن السهل جدا قراءة مستندات XML ونقلها او حملها من جهاز إلى آخر. وقد يظن البعض ان اعتماد XAML على XML له اثر على الأداء ، فمن المعروف ان XML لديها بعض المشاكل المتعلقة بهذا الشأن نظرا لأن المستند يمكن ان يحتوي على الكثير من الوسوم ما يجعل عملية الأعراب تأخذ وقتا اطول. وللتغلب على هذه المشكلة فإن WPF تقوم بتحويل مستند XAML إلى الصورة الثنائية المكافئة له والتي تسمى بـ BAML (اختصار لـ Binary Application Markup Language)، وهذه التحويل يحدث بدون ان يشعر المبرمج خلال عملية الترجمة. اساسيات XAML: حتى نفهم XAML ، علينا ان نتذكر القواعد التالية: - كل عنصر في مستند XAML يتم ترجمته إلى مثيل من فئة في الدوت نت. ودائما يكون اسم العنصر مطابقا لأسم فئة معينة، مثلا العنصر <Button> يمثل امر لـ XAML من اجل إنشاء كائن من الفئة Button. - كما هو الحال في اي مستند XML ، يمكن للعناصر ان تتداخل، وتقوم XAML بتفسير هذا التداخل على انه احتواء ، فإذا وجدت عنصر Button داخل عنصر Grid ، فإن هذا يعني ان الواجهة سوف تتضمن Grid يحتوي على زر - يمكن تعيين الخصائص من خلال الواصفات (Attributes) ، لكن في بعض الحالات تكون الواصفة غير كافية لتحديد قيمة الخاصية ، لذا فإننا نستخدم صيغة اخرى تسمى بالعنصر الخاصية (Property Element). لكن قبل ان ندخل في التفاصيل ، لنحاول اولاً ان نلقي نظرة على مستند XAML يمثل نافذة جديدة في الفيجوال استديو(الأسطر مرقمة للتوضيح فقط): 1 <Window x:Class="WpfApplication1.Window1" 2 xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" 3 xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" 4 Title="Window1" Height="300" Width="300"> 5 <Grid> 6 </Grid> 7 </Window> كما تلاحظ إن هذا المستند يحتوي على عنصرين فقط ، هما العنصر الجذر والمتمثل في النافذة والعنصر الآخر هو Grid يمكن وضع عناصر اخرى بداخله مثل الكنترولات والأشكال والصور وغيرها. وبشكل عام يمكن ان يكون العنصر الجذر من اي نوع ، لكنه في WPF غالبا ما يكون احد الأنواع التالية: - Window - Page - Application - ResourceDictionary وكما هو الحال مع اي مستند XML فإن كل مستند XAML يجب ان يحتوي على عنصر جذر واحد فقط ، ما يعني انه بمجرد ان نغلق عنصرالنافذة بالوسم </Window> فإن المستند ينتهي عند هذا الحد ولا يمكن ان يأتي اي شئ بعده. إذا نظرنا إلى الوسم الأفتتاحي للنافذة فإننا سوف نجد العديد من الواصفات التي تم استخدامها، فهناك واصفة الفئة المقابلة ، وواصفتين من اجل تحديد مجالين للاسماء ، بينما الواصفات: 4 Title="Window1" Height="300" Width="300"> يمثل كل منها خاصية معينة في النافذة وبالتالي فإن النافذة يكون عنوانها Window1 وحجمها 300 في 300 وحدة. مجالات الأسماء في XAML من المعروف ان الفئات في الدوت نت تكون داخل مجالات الأسماء(Namespaces) ، وحتى تستطيع XAML تحديد الفئة المقابلة فإنها تحتاج إلى اسم الفئة واسم المجال الذي يحتويها. وكما عرفنا إن اسم الفئة يتم تحديده في اسم العنصر ، بينما اسم المجال يتم تحديده من خلال الواصفة xmlns الخاصة بـ XML . وعندما ننظر في مستند XAML السابق فإننا نجد ان هناك مجالين اسماء هما: 2 xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" 3 xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" هذين المجالين سوف نجدهما في معظم مستندات XAML وكل مجال يؤدي وظيفة معينة: - المجال http://schemas.microsoft.com/winfx/2006/xaml/presentation وهو خاص بـ WPF و يتضمن جميع الفئات في WPF بما فيها الكنترولات التي سوف نستخدمها من اجل إنشاء الواجهة. هذا المجال تم التصريح عنه بدون اي بادئة لذا يتم التعامل معه على انه المجال الأفتراضي في المستند، واي عنصر سوف ينتمي لهذا المجال بشكل افتراضي. - المجال http://schemas.microsoft.com/winfx/2006/xaml وهو خاص بـ XAML ويتضمن بعض الوظائف والعناصر المساعدة في إنشاء المستند. نلاحظ ان هذا المجال قد تم التصريح عنه مع البادئة x ، وهذا يعني انه من اجل استخدام اي عنصر ينتمي له فيجب ان نكتب اسمه مسبوقا بتلك البادئة ، مثل : <x:ElementName>. هذا النوع من التصريح عن مجال الأسماء يسمى بالمجال القائم على اساس URI ، وهناك نوع آخر سوف نتطرق له لاحقا، لكن ما يميز هذه الطريقة ان المجال في XML يتضمن اكثر من مجال مقابل في الدوت نت ، مثلا http://schemas.microsoft.com/winfx/2006/xaml/presentation يتضمن الكثير من المجالات في الدوت نت (جميعها تبدأ بـ System.Windows). Code-Behind نستطيع من خلال XAML إنشاء وتعريف واجهة المستخدم ، لكن من اجل تعريف الوظائف في التطبيق التي ستستجيب لأحداث المستخدم فإننا نحتاج إلى كتابة الكود . ربط مستند XAML بفئة في كود التطبيق يتم من خلال الواصفة x:Class التي تكون صيغتها بهذا الشكل: <object x:Class="namespace.classname"...> ... </object> في المثال السابق سوف نجد: 1 <Window x:Class="WpfApplication1.Window1" وهي تخبر XAML بإن يقوم بإنشاء فئة جديدة اسمها Windows1 في المجال WpfApplication1 وتكون مشتقة من الفئة Window . لاحظ ان هذه الواصفة تبدأ بـ x ما يعني انها تنتمي إلى المجال الخاص بـ XAML ويمكن استخدامها مع العنصر الجذر فقط. يتم تجزئة الفئة Windows1 في ملفين هما: - الملف الأول(Window1.g.cs) : يحتوي على الكود الذي يتم توليده تلقائيا في زمن الترجمة (compile time). - الملف الثاني (Window1.xaml.cs): من اجل ان نقوم بتعريف الدوال التي تعالج الأحداث التي تصدر من العناصر في النافذة، وهذا الملف هو الذي سنتعامل معه بشكل اساسي. عندما نقوم بالقاء نظرة على الملف Window1.xaml.cs سوف نجده بهذا الشكل: namespace WpfApplication1 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } } } وكما نرى ان هذه الفئة حتى الآن لا تحتوي على اي دالة باستثناء المشيد الأفتراضي الذي يقوم باستدعاء الدالة InitializeComponent() ، وهذه الدالة يتم توليدها تلقائيا خلال عملية الترجمة ، ولا تحتاج منا إلى اي تعديل (في الواقع اي تعديل عليها بشكل يدوي سوف يختفي خلال عملية التوليد التالية). وإذا احتجنا إلى ان نضيف كود إلى المشيد ، فيجب ان نتأكد انه يقوم باستدعاء InitializeComponent(). تسمية العناصر: غالبا ما نحتاج إلى كتابة الكود (Code-Behind) الذي يقوم بتغيير خصائص عنصر او اكثر في واجهة المستخدم ، ويمكننا الوصول إلى اي عنصر بشرط ان نقوم بتسميته. تسمية العناصر في XAML تتم من خلال الواصفة x:Name التي يمكن استخدامها مع اي عنصر يمثل كائن، فلو اردنا ان نسمي العنصر Grid: <Grid x:Name="grid1"> ويقوم XAML تلقائيا بإضافة حقل إلى الفئة يمثل ذلك العنصر: internal System.Windows.Controls.Grid grid1; ومن خلال هذا الحقل يمكننا التعامل مع العنصر Grid MessageBox.Show(grid1.ActualHeight.ToString()); تذكر ان الواصفة x:Name تنتمي إلى لغة XAML ، ويمكن تطبيقها على اي عنصر في XAML . استخدام الخاصية Name في العناصر: الكثير من العناصر في WPF تحتوي على الخاصية Name . ومثل هذا العناصر يمكن تسميتها سواء باستخدام الواصفة x:Name او باستخدام الخاصية Name . فالعنصر Grid على سبيل المثال يحتوي على الخاصية Name (المعرفة اساسا في الفئة الأب FrameworkElement) ويمكن تسميته من خلالها بالشكل التالي: <Grid Name="grid1"> وسواء استخدمت الواصفة x:Name او الخاصية Name فإن النتيجة تكون متساوية ، حيث يتم تحديد اسم الحقل وتحديد قيمة الخاصية Name . التعامل مع الخصائص البسيطة: اصبح معروفا لدينا ان الواصفات في العنصر تقوم بتعيين الخصائص في الكائن المقابل في الدوت نت. فلو اردنا إنشاء زر يحتوي على النص "This is a button" ويكون لون الخط احمر وخلفيته زرقاء ، فإننا نكتب: <Button Background="Blue" Foreground="Red" Content="This is a button"/> ويمكن استخدام اي واصفة في XAML ، بشرط ان يكون لها خاصية مقابلة تنتمي إلى فئة الكائن في الدوت نت، وسوف تعترض XAML على اي واصفة لا تكون كذلك. يمكننا ايضاً عمل نفس الزر السابق من خلال كود C# بالشكل التالي: Button btn = new Button(); btn.Background = Brushes.Yellow; btn.Foreground = Brushes.Red; btn.Content = "This is a button"; محولات الأنواع: تكون القيمة الواصفة دائما عبارة عن نص ، و المشكلة ان نوع الخصائص المقابلة قد يكون من اي نوع ، ففي الزر السابق ، سوف نجد ان الخاصية Background من النوع Brush . وحتى تستطيع XAML تعيين مثل هذه الخصائص فلا بد لها اولا ان تقوم بتحويل القيمة النصية إلى قيمة مكافئة من نوع الخاصية المقابلة. عملية التحويل تتم من خلال محولات النوع (TypeConverters) التي تقوم اساسا بتحويل قيمة من نوع إلى آخر. مثلا المحول BrushConverter مختص بتحويل القيمة من وإلى كائن من النوع Brush. إذا لم يستطع المحول تحويل القيمة ، او إذا لم تستطع XAML تحديد المحول المرافق للخاصية (او نوعها) فإنها تقوم بتوليد خطأ وتتوقف العملية. لاحظ ايضاً ان XAML متحسسة لحالة الأحرف فيما يتعلق باسماء العناصر والواصفات، بينما لا تكون متحسسة لحالة الأحرف مع القيم ، لإن محولات الأنواع يمكنها تحويل القيمة النصية بغض النظر عن حالة الأحرف فيها. التعامل مع الخصائص المركبة هناك بعض الخصائص التي لا يمكن تعيينها من خلال الواصفات ، بسبب ان قيمتها قد تمثل كائن او عدة كائنات يصعب التعبير عنها بنص بسيط. لهذا وفرت XAML صيغة اخرى لتعيين الخصائص في الكائنات تسمى بـ "صيغة العنصر الخاصية" (Property Element Syntax) ، وفيها نقوم بإضافة عنصر XML يمثل الخاصية ويكون اسمه على شكل <TypeName.Property> وينتهي بـ </TypeName.Property> ، ويتم تحديد القيمة او القيم المطلوبة داخل هذا العنصر. مثلا لو اردنا إنشاء نفس الزر السابق باستخدام العنصر الخاصية فإننا نكتب: <Button> <Button.Background> <SolidColorBrush Color="Blue"/> </Button.Background> <Button.Foreground> <SolidColorBrush Color="Red"/> </Button.Foreground> <Button.Content> This is a button </Button.Content> </Button> لاحظ انه من خلال "العنصر الخاصية" نستطيع ان نحدد او نخصص نوع الكائن الذي يمثل القيمة واي خصائص اخرى بداخله. فلو اردنا ان نجعل الزر محتويا على صورة بدل النص فإننا سوف نقوم بإضافة عنصر يمثل الصورة بداخل الخاصية: <Button> <Button.Content> <Image Source="help.png"/> </Button.Content> </Button> و لو اردنا استخدام Brush من نوع LinearGradientBrush لكي نجعل لون الخلفية بلون متدرج فإننا سوف نستخدم : <Button> <Button.Background> <LinearGradientBrush> </LinearGradientBrush> </Button.Background> </Button> ومن اجل ان يعمل هذا التدرج بشكل سليم، فلا بد من تحديد الألوان في LinearGradientBrush ، من خلال تعبئة الخاصية GradientStops والتي تمثل كولكشن كائنات من النوع GradientStop، ومثل هذه الخاصية يمكن تعيينها في XAML من خلال "العنصر الخاصية" فقط: <Button> <Button.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Button.Background> </Button> يمكننا بعد ذلك إضافة الكائنات GradientStop وتحديد الألوان المطلوبة : <Button> <Button.Background> <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.50" Color="Indigo" /> <GradientStop Offset="1.00" Color="Violet" /> </LinearGradientBrush.GradientStops> </LinearGradientBrush> </Button.Background> </Button> يكون الكود المكافئ بهذا الشكل: LinearGradientBrush brush = new LinearGradientBrush(); GradientStop gradientStop1 = new GradientStop(); gradientStop1.Offset = 0; gradientStop1.Color = Colors.Red; brush.GradientStops.Add(gradientStop1); GradientStop gradientStop2 = new GradientStop(); gradientStop2.Offset = 0.5; gradientStop2.Color = Colors.Indigo; brush.GradientStops.Add(gradientStop2); GradientStop gradientStop3 = new GradientStop(); gradientStop3.Offset = 1; gradientStop3.Color = Colors.Violet; brush.GradientStops.Add(gradientStop3); button.Background = brush; يمكننا ايضاً مع بعض الخصائص (التي يكون لها set) تحديد نوع الكولكشن المستخدم : <LinearGradientBrush> <LinearGradientBrush.GradientStops> <GradientStopCollection> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.50" Color="Indigo" /> <GradientStop Offset="1.00" Color="Violet" /> </GradientStopCollection> </LinearGradientBrush.GradientStops> </LinearGradientBrush> خصائص الأحتواء: تسمح XAML لأي فئة بإن تحدد احدى الخصائص فيها كخاصية احتواء افتراضية، واي عنصر من تلك الفئة يمكن ان يحتوي على عنصر او عدة عناصر بداخله مباشرة بدون الحاجة إلى تحديد عنصر الخاصية. مثلا خاصية الأحتواء الأفتراضية في الزر هي الخاصية Content لذا يمكننا عمل نفس الزر السابق بدون تحديد هذه الخاصية : <Button> <Image Source="help.png"/> </Button> ايضا خاصية الأحتواء الأفتراضية في LinearGradientBrush هي GradientStops فيمكننا إنشاء التدرج السابق بالشكل التالي: <LinearGradientBrush> <GradientStop Offset="0.00" Color="Red" /> <GradientStop Offset="0.50" Color="Indigo" /> <GradientStop Offset="1.00" Color="Violet" /> </LinearGradientBrush> تعددت الطرق والنتيجة واحدة: مما سبق ، يمكن تعيين اي خاصية باكثر من طريقة ، مثلا من اجل تعيين الخاصية Content في الزر فإننا نستخدم اي طريقة من التالي: <Button Content="Click Me"/> <Button> <Button.Content> Click Me </Button.Content> </Button> <Button> Click Me </Button> الخصائص المرفقة (Attached Properties) بإلاضافة إلى الخصائص الاعتيادية والتي نتعامل معها دائما ، قامت XAML بإضافة مبدأ جديد يسمى بـ "الخصائص المرفقة" ، وهي خصائص يمكن تطبيقها على العديد من العناصر رغم انها معرفة اصلا في فئات مختلفة. عادة ما تستخدم الخصائص المرفقة من خلال الواصفات حيث يكون اسم الواصفة على الشكل OwnerType.PropertyName وهذه الصيغة تشبه إلى حد كبير الأسم في "صيغة العنصر الخاصية" لكن الفئة المالكة للخاصية هنا (OwnerType) غالبا ما تكون مختلفة عن فئة العنصر الكائن. تستخدم الخصائص المرفقة في WPF كثيرا مع الواح التخطيط (Layout Panels) مثل Grid و DockPanel. مثلا من اجل تحديد رقم الصف الذي سوف يوضع فيه العنصر داخل Grid فإننا نستخدم الخاصية المرفقة Grid.Row: <Button Grid.Row="1" Content="Click Me"/> لا تقوم الفئة المالكة للخاصية المرفقة بتعريف خاصية مقابلة بداخلها ، انما تقوم بتعريف دالة لتعيين القيمة تكون على شكل SetPropertyName() ودالة اخرى لأحضار القيمة تكون على شكل GetPropertyName(). مثلا الخاصية المرفقة Grid.Row يتم تحقيقها بهذا الشكل: public class Grid { public static int GetRow(UIElement element); public static void SetRow(UIElement element, int value); } كما يمكننا تعيين اي خاصية مرفقة من خلال الكود على هذا النحو: Grid.SetRow(btn, 1); تمديدات الوسوم (Markup Extensions) من خلال الواصفات او العناصر الخاصية نقوم بتعيين خاصية الكائن إلى قيمة جديدة دائما. بعض الحالات تتطلب منا تعيين الخاصية إلى قيمة موجودة من قبل ، او تعيينها إلى القيمة الخالية (null)، او ربط الخاصية بخاصية اخرى في كائن آخر ، ومثل هذه العمليات وغيرها يمكن إنجازها من خلال ما يسمى بـ" تمديدات الوسوم" (Markup Extensions) . جميع التمديدات مشتقة من الفئة MarkupExtension (في المجال System.Windows.Markup)، وغالبا ما يكون اسمها منتهيا باللاحقة Extension التي يمكن اهمالها مع XAML. غالبا ما تستخدم تمديدات الوسوم (Markup Extensions) مع الواصفات ، حيث يكون تمديد الوسم محاطاً بمربعات متعرجة { }. المثال التالي على بعض تمديدات الوسوم (Markup Extensions): <Button Background="{x:Null}" Height="{x:Static SystemParameters.IconHeight}" Content="{Binding Path=Height, RelativeSource={RelativeSource Self}}"/> x:Null التعيين إلى القيمة الخالية (null). حيث قمنا في هذا المثال بتعيين الخاصية Background إلى القيمة الخالية. x:Static التعيين إلى خاصية ساكنة في فئة في الدوت نت. في المثال قمنا بتعيين ارتفاع الزر إلى الخاصية الساكنة IconHeight في الفئة SystemParameters. Binding من اجل عمليات الربط (Data Binding) في WPF . وكما نرى فإن هناك تمديدات تنتمي إلى XAML (تبدأ بـ x) واخرى تنتمي إلى WPF، كما يمكننا تعريف تمديدات مخصصة من خلال الأشتقاق من الفئة MarkupExtension. ملاحظة: إذا اردنا ان نعين الواصفة إلى نص يبدأ بقوس الأفتتاح { ، فإن علينا استخدام حروف الهروب. مثلا لو اردنا تعيين الخاصية Content في الزر إلى النص "{some text}" فإننا نكتبها بهذه الطريقة : <Button Content="{}{some text}"/> ولا نحتاج إلى اي حروف هروب مع العنصر الخاصية: <Button> {some text} </Button> الأحداث يمكننا من خلال الواصفات ايضاً تحديد الإجراء او الدالة المعالجة لحدث في عنصر معين. وتكون الصيغة بهذا الشكل: <Button ... Click="Button_Click "> ويجب ان يحتوي الكود(Code-Behind) على الدالة Button_Click يتفق توقيعها مع توقيع الحدث: private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show(txtMessage.Text); } يقدم فيجوال استديو بعض الأدوات المساعدة (IntelliSense) مع الأحداث، فبمجرد ان تكتب اسم الحدث متبوعا بعلامة يساوي ، فإنه تظهر قائمة تتضمن إنشاء دالة حدث جديدة او بعض الدوال الأخرى الموجودة مسبقا . لاحظ اننا نستخدم الواصفات من اجل تعيين الخصائص و الأحداث في الكائن، خلال عملية الأعراب يتم تعيين الأحداث قبل الخصائص في الكائن، باستثناء الخاصية Name التي يتم تعينها اولا(في حال استخدمت فعلاً)، ما يعني ان اي حدث مرتبط بتغيرات خاصية (مثل TextChanged ) سوف يقدح خلال هذه العملية. استخدام فئات من مجالات اسماء اخرى: إلى جانب التصريح عن مجال الأسم من خلال URI ، يوجد نوع آخر من التصريح يستخدم من اجل تحديد مجال اسم بعينه في الدوت نت . وتكون صيغته بهذا الشكل: xmlns:Prefix="clr-namespace:Namespace;assembly=AssemblyName" حيث: - Prefix: تمثل البادئة التي نريد استخدامها عند التعامل مع الكائنات التي تنتمي لهذا المجال. - Namespace: يمثل مجال الأسم الذي نريد التصريح عنه. - AssemblyName: اسم التجمع الذي يحتوي على مجال الأسم. يمكننا اهمال هذا الجزء إذا كان المجال ينتمي إلى المشروع الحالي. عادة ما يتم التصريح عن المجالات في العنصر الجذر في المستند. مثلا من اجل التصريح عن المجال MyNamespace في المشروع الحالي: xmlns:local="clr-namespace:MyNamespace" ومن اجل إنشاء كائن في المجال MyNamespace: <local:MyObject ...></local:MyObject> ومن اجل التصريح عن المجال System في الدوت نت: xmlns:sys="clr-namespace:System;assembly=mscorlib" ومن اجل إنشاء كائن من النوع DateTime : <sys:DateTime>10/30/2010 4:30 PM</sys:DateTime> =========================================== المراجع: - Pro WPF in C# 2008 - Windows Presentation Foundation Unleashed
  3. Wpf Layout - Part 2

    جزاك الله خيرا اخي الكريم بالتأكيد احتاج إلى المساعدة،, WPF موضوع كبير جدا ، وانا على يقين بان في المنتدى من لديه الخبرة والمعرفة في التعامل مع WPF ، وانتظر مشاركاتهم بفارغ الصبر . تحياتي,,
  4. ابحث عن Guid (اختصار لـ Globally Unique IDentifier) .
  5. C# Class Name As Parameter Object

    العفو :)
  6. C# Class Name As Parameter Object

    ايضاً ، يمكنك ان تجرب هذا : object obj = Activator.CreateInstance(Type.GetType("ConsoleApplication1.c1"));
  7. Wpf Layout - Part 2

    وكل عام وانت بخير اخي العزيز,, بالنسبة لسؤالك,, نعم يجب ان تتعلم XAML، على الرغم من ان الشئ الذي نقوم بعمله XAML يمكننا ايضاً القيام به من خلال كود C# او VB ، لكن المشكلة في حجم الكود الذي سوف تحتاج إلى كتابتة ، فما نستطيع ان نقوم به في بضعة اسطر في XAML قد يقابله الكثير من الأسطر في الكود المكافئ بـ C# او VB (ويحتاج إلى مزيد من الخبرة). والفكرة في WPF ان نقوم بتعريف واجهة المستخدم من خلال XAML ، بينما نقوم بتعريف السلوك او المنطق المطلوب لتلك الواجهة من خلال الكود (Code-Behind) .
  8. Wpf Layout - Part 2

    التداخل في الألواح: كما ذكرنا سابقاً انه يمكن لأي لوح ان يحتوي على لوح آخر ، ويقوم اللوح الأب بالتعامل مع اللوح الأبن بنفس الطريقة التي يتعامل بها مع اي عنصر ابن آخر. واستخدام اكثر من لوح هو امر شائع في WPF من اجل إنشاء التخطيط المطلوب، فعلى سبيل المثال لو اردنا إنشاء مربع حوار يتكون من زرين موافق والغاء الأمر يكونان في الأسفل ومربع نص يشغل المساحة المتبقية في النافذة فإننا نستخدم: <DockPanel LastChildFill="True"> <StackPanel DockPanel.Dock="Bottom" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Margin="10,10,2,10" Padding="3">OK</Button> <Button Margin="2,10,10,10" Padding="3">Cancel</Button> </StackPanel> <TextBox DockPanel.Dock="Top" Margin="10">This is a test.</TextBox> </DockPanel> النتيجة استخدام Grid: يعتبر هذا اللوح من افضل الألواح على الأطلاق ويمكننا بواسطته عمل كل شئ يقوم به اي لوح آخر(تقريباً). يقوم هذا اللوح بتقسيم المساحة المعطاة له إلى صفوف واعمدة غير مرئية ، ويمكن وضع العناصر داخل الخلايا (الناتجة من تقاطع الصف مع العمود) . والجميل هنا انه يمكن وضع اكثر من عنصر في نفس الخلية، وبشكل افتراضي كل Grid يتكون من صف واحد وعمود واحد ، ما يعني بانه يتكون من خلية واحدة بشكل افتراضي. من اجل اضافة الصفوف و الأعمدة إلى Grid ، فإن علينا استخدام الخاصيتين Grid.RowDefinitions و Grid.ColumnDefinitions ، فلو اردنا اضافة صفين وثلاثة اعمدة فإننا نستخدم: <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> </Grid> تكون الصفوف متساوية في الأرتفاع ، والأعمدة متساوية في العرض بشكل افتراضي. ملاحظة: لاحظ انه يجب استخدام العنصر Grid.RowDefinitions ، والعنصر Grid.ColumnDefinitions من اجل تحديد الصفوف والأعمدة المطلوبة. تسمى هذه الصيغة في Xaml بـ "صيغة العنصر الخاصية" (Property Element Syntax) وهي طريقة اخرى لتعيين الخصائص في الكائن إلى جانب "صيغة الواصفة" (Attribute Syntax) . بعد ذلك يمكننا إضافة العناصر إلى Grid وتحديد الصف والعمود الذي يقع فيه كل عنصر عن طريق الخصائص المرفقة Grid.Row و Grid.Column: <Grid> ……… <Button Grid.Row="0" Grid.Column="0">Top Left</Button> <Button Grid.Row="0" Grid.Column="1">Middle Left</Button> <Button Grid.Row="1" Grid.Column="2">Bottom Right</Button> <Button Grid.Row="1" Grid.Column="1">Bottom Middle</Button> ……… </Grid> الصفوف والأعمدة في Grid ترقم على الأساس الصفري ، بمعنى ان الصف الأول يكون رقمه 0 والصف الثاني يكون رقمه 1 وهكذا. إذا لم نقم بتحديد الصف او العمود لعنصر معين فإن هذا العنصر يتم وضعه في الصف الأول والعمود الأول ، بسبب ان القيمة الأفتراضية للخصائص المرفقة Grid.Row و Grid.Column هي صفر(بمعنى الأول). حجم الأعمدة والصفوف: تقدم Grid ثلاث طرق لتحديد ارتفاع الصف وعرض الأعمدة: - الحجم المطلق (Absolute size): حيث يتم تعيين رقم يمثل عدد الوحدات التي يشغلها العرض او الأرتفاع <ColumnDefinition Width="100"/> - الحجم التلقائي(Automatic size): حيث يعطى الصف او العمود القدر الذي يحتاج اليه تماما (استنادا على حجم المحتوى) <ColumnDefinition Width="Auto"/> - الحجم النسبي(Proportional size): حيث يتم تقسيم المساحة المعطاة على الصفوف اوالأعمدة <ColumnDefinition Width="*"/> وبشكل افتراضي، يكون التناسب متساويا ما لم نقم بإضافة رقم معين يسبق النجمة مثل: <RowDefinition Height="*"/> <RowDefinition Height="2*"/> بهذا تكون نسبة الصف الأول إلى الصف الثاني هي 1 إلى 2. عمل مربع الحوار السابق باستخدام Grid: باستخدام هذه الطرق في التحجيم فإنه يمكننا استخدام Grid من اجل إنشاء نفس مربع الحوار السابق على النحو التالي : <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <TextBox Margin="10" Grid.Row="0">This is a test.</TextBox> <StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Margin="10,10,2,10" Padding="3">OK</Button> <Button Margin="2,10,10,10" Padding="3">Cancel</Button> </StackPanel> </Grid> الخاصية ShowGridLines: يمكن عرض خطوط الشبكة في Grid بتعيين هذه الخاصية إلى True (كما يظهر في المثال السابق). الخصائص المرفقة Grid.RowSpan و Grid.ColumnSpan يمكننا ان نجعل العنصر يشغل اكثر من صف واحد او عمود واحد من خلال الخصائص Grid.RowSpan و Grid.ColumnSpan، فعلى سبيل المثال لو اردنا ان يكون الزر في الصف الأول والعمود الأول ويشغل صفين وعمودين فإننا نكتب : <Button Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2">Span Button</Button> تقسيم النوافذ باستخدام GridSplitter باستخدام GridSplitter يمكننا ان نجعل المستخدم قادرا على تغيير عرض العمود او ارتفاع الصف بنفس الطريقة التي تظهر في نوافذ مثل Windows Explorer حيث يكون بالأمكان تغيير عرض قائمة المجلدات عن طريق سحب حافتها في الأتجاه المطلوب. إضافة GridSplitter إلى Grid عملية سهلة رغم ان الطريقة غريبة بعض الشئ، ومن الجيد ان نتذكر التالي: - يجب وضع GridSplitter داخل خلية في Grid ، هذه الخلية يمكن ان تحتوي على عناصر اخرى وفي هذه الحالة يجب ان نتأكد من هذه العناصر لا تقوم بإخفاء GridSplitter (بمعنى تغطيه) عن طريق تعيين الهوامش (Margin). والأفضل ان نقوم بعمل عمود او صف مخصص لـ GridSplitter - لا يقوم GridSplitter بتعديل حجم الخلية فقط انما يقوم بتعديل حجم الصف او العمود بالكامل، ولكي نجعل هذا السلوك ينسجم مع المظهر في UI فإن علينا ان نجعل GridSplitter يمتد على طول الصف او العمود المطلوب باستخدام الخصائص Grid.RowSpan و Grid.ColumnSpan. - مبدأيا يكون GridSplitter غير مرئيا ، ومن اجل ان نجعله مرئيا فلا بد من اعطائه حجم معين ينسجم مع الأتجاه الذي يعمل عليه. عن طريق اعطائه عرض معين (مع الأعمدة) او طول معين (مع الصفوف). - تحدد محاذاة GridSplitter طريقة عمله ، بمعنى هل سيعمل كمقسم افقي (لتغيير حجم الصفوف) او كمقسم رأسي (لتغيير حجم الأعمدة) ، فمن اجل ان يعمل كمقسم افقي فإنه يجب تعيين الخاصية VerticalAlignment إلى Center بحيث يتم تغيير حجم الصف في الأعلى والصف في الأسفل. ومن اجل ان يعمل كمقسم رأسي فإنه يجب تعيين الخاصية HorizontalAlignment إلى Center بحيث يتم تغيير حجم العمود في اليسار والعمود في اليمين. (يمكن ايضاً استخدام الخصائص ResizeDirection و ResizeBehavior في GridSplitter من اجل تحديد السلوك المطلوب) المثال التالي يوضح كيفية عمل مقسم رأسي لتغيير حجم الأعمدة : <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth="100"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition MinWidth="50"></ColumnDefinition> </Grid.ColumnDefinitions> <Button Grid.Row="0" Grid.Column="0" Margin="3">Left</Button> <Button Grid.Row="0" Grid.Column="2" Margin="3">Right</Button> <Button Grid.Row="1" Grid.Column="0" Margin="3">Left</Button> <Button Grid.Row="1" Grid.Column="2" Margin="3">Right</Button> <GridSplitter Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Width="3" VerticalAlignment="Stretch" HorizontalAlignment="Center" ShowsPreview="False"></GridSplitter> </Grid> الخاصية ShowsPreview : تكون قيمة الخاصية ShowsPreview في GridSplitter هي False بشكل افتراضي ، مايعني ان GridSplitter سوف يقوم بتغيير حجم العمود او الصف مباشرة خلال عملية السحب، وعند تغيير هذه الخاصية إلى True فإن GridSplitter سوف يسمح للمستخدم ان يقوم بمعاينة الحجم المحدد اولا وعند تحرير زر الماوس فإن الحجم الجديد يتم تطبيقه.
  9. Wpf Layout - Part 2

    في الجزء الأول ، قمنا بالقاء نظرة سريعة على عملية التخطيط (Layout) في WPF وشرحنا طريقة استخدام احد الألواح هو الـ StackPanel، بإذنه تعالى ، في هذا الجزء سوف نحاول ان نكمل شرح بقية الألواح. استخدام WrapPanel: يقوم اللوح WrapPanel بترتيب العناصر في صفوف او اسطر افقية او رأسية . فيتم ملئ السطر الأول ثم السطر الثاني وهكذا. واتجاه الترتيب يتم تحديده بواسطة الخاصية Orientation التي تكون قيمتها Horizontal بشكل افتراضي ويمكن تغييرها إلى القيمة Vertical مثال : <WrapPanel> <Button>Button 1</Button> <Button>Button 2</Button> <Button>Button 3</Button> <Button>Button 4</Button> <Button>Button 5</Button> </WrapPanel> النتيجة: لاحظ كيف يتغير ترتيب العناصر عند تكبير وتصغير النافذة لنأخذ مثالا آخر ، مع التغيير في بعض خصائص العناصر. مثال : <Window x:Class="LayoutPanels.WrapPanelExample2" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="WrapPanelExample2" Height="300" Width="300"> <WrapPanel Margin="3"> <Button VerticalAlignment="Top">Top Button</Button> <Button MinHeight="60">Tall Button 2</Button> <Button VerticalAlignment="Bottom">Bottom Button</Button> <Button>Stretch Button</Button> <Button VerticalAlignment="Center">Centered Button</Button> </WrapPanel> </Window> النتيجة: كما هو مبين اعلاه، يكون ارتفاع كل صف بارتفاع العنصر الأكبر بداخله. استخدام DockPanel في الكثير من التطبيقات (مثل وورد، اكسل ...الخ) نجد شريط الأدوات قد تم رصفه في اعلى النافذة ، بينما شريط الحالة في اسفل النافذة. في WPF يمكننا عمل مثل هذا التخطيط عن طريق DockPanel التي تقوم برصف العناصر إلى اي جانب من جوانب النافذة ، وتحديد الجانب المطلوب يتم عن طريق الخاصية المرفقة DockPanel.Dock التي يمكن ان تأخذ قيمة من القيم التالية (Left، Top، Right، Bottom) . المثال التالي يقوم بوضع زر على كل جانب : <Window x:Class="LayoutPanels.DockPanelExample1" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="DockPanelExample1" Height="300" Width="300"> <DockPanel> <Button DockPanel.Dock="Top">الزر - اعلى</Button> <Button DockPanel.Dock="Bottom">الزر-اسفل</Button> <Button DockPanel.Dock="Left">الزر- يسار</Button> <Button DockPanel.Dock="Right">الزر- يمين</Button> <Button>المساحة المتبقية</Button> </DockPanel> </Window> النتيجة: لاحظ ان المساحة المتبقية قد تم اعطائها للزر الأخير وهذا بسبب ان الخاصية LastChildFill في DockPanel تكون قيمتها True بشكل افتراضي- ويمكنك تغيير قيمتها إذا لم يكن هذا مناسب بالنسبة لك. تذكر ايضاً ان ترتيب العناصر يلعب دورا هاما عند التعامل مع DockPanel، فلو عدنا إلى المثال السابق ، سوف نجد ان الزرين الأعلى والأسفل قد احتل كل منهما المساحة الكاملة من الجانب المطلوب.بينما الزرين الأيمن والأيسر قد شغلا المساحة المتبقية من الجانب. وسبب هذا اننا وضعنا الزرين الأعلى والأسفل قبل الزرين الأيمن والأيسر. والنتيجة سوف تتغير عندما نقوم بتغيير الترتيب بحيث نضع الزرين الأيمن والأيسر اولاً: يمكننا ايضاً رصف اكثر من عنصر في نفس الجانب ، وتحديد الهامش او المحاذاة سواء الأفقية او الرأسية لأي عنصر: <Window x:Class="LayoutPanels.DockPanelExample2" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="DockPanelExample2" Height="300" Width="300"> <DockPanel LastChildFill="True"> <Button DockPanel.Dock="Top">زر- اعلى - ممتد</Button> <Button DockPanel.Dock="Top" HorizontalAlignment="Center"> زر- اعلى- توسيط</Button> <Button DockPanel.Dock="Top" HorizontalAlignment="Left"> زر-اعلى- محاذي لليسار</Button> <Button DockPanel.Dock="Bottom">زر- اسفل</Button> <Button DockPanel.Dock="Left">زر-يسار</Button> <Button DockPanel.Dock="Right">زر- يمين</Button> <Button>المساحة المتبقية</Button> </DockPanel> </Window>
  10. في البداية يجب ان نعرف بإن المقصود بـ Layout هو عملية تحديد حجم وموقع كل عنصر في واجهة المستخدم بحيث ينتج الشكل النهائي المطلوب لتلك الواجهة. وهناك العديد من الكلمات العربية المرادفة لكلمة Layout مثل تخطيط ، تنظيم، تشكيل . وفي كل الأحوال اخترت في هذه المشاركة ان تكون كلمة تخطيط هي الكلمة المرادفة لـ Layout واعتذر ان كنت قد اخطأت في هذا الأختيار ، ايضاً هناك العديد من الكلمات الأنجليزية الأخرى التي لم استطع تحديد الكلمة العربية المرادفة لها ، او لا توجد كلمة عربية متفقا عليها ، لذا تجدني كثيرا ما اضع الكلمة العربية وبجوارها الكلمة الأنجليزية بين قوسين. افترض ان لديك: - خلفية ولو بسيطة عن WPF و XAML - فيجوال استديو 2008 لماذا كتبت عن هذا الموضوع؟ ومن وجهة نظري الشخصية ، ان موضوع التخطيط (Layout) في WPF هو الصعب السهل ، فهو صعب لإن الطريقة المتبعة في WPF مختلفة بعض الشئ عن ما اعتدنا عليه في تقنيات سابقة مثل Windows Form وغيرها، وسهل لأن العملية بمجملها تعني اختيار احد ألواح التخطيط ثم وضع العناصر بداخلها(كما سنرى). وقد يكون هذا احد الأسباب التي دفعتني لأن اكتب حول هذا الموضوع. فيما مضى: تعتبر عملية التخطيط (Layout) من اهم العمليات في تصميم واجة المستخدم حيث اننا نقضي معظم الوقت في تحديد حجم وموضع الكنترولات في النافذة لكي يكون الشكل النهائي جذاب ، عملي ومرن في نفس الوقت. لكن التحدي الحقيقي والصعوبة الحقيقية تكمن في التأكد من ان هذا التخطيط يمكن ان يتكيف تلقائيا عندما يتغير حجم النافذة او تتغير دقة الشاشة. في الإصدار الأول Windows Form كانت عملية التخطيط تتم عن طريق تحديد موقع وحجم كل كنترول ، بالإضافة إلى امكانية استخدام خصائص الإرساء (anchoring) والرصف (docking) التي ساعدتنا كثيرا خلال هذه العملية. ورغم جمال هاتين الخاصيتين ومساعدتها لنا في إنشاء نوافذ و مربعات حوار بسيطة ، إلا انها لم تكن مفيدة في حالات اخرى كثيرة. مثل تقسيم مساحة من النافذة إلى منطقتين متساويتين ومتجاورتين في نفس الوقت (bi-pane)، كذلك فإنها لم تكن مفيدة في الحالات التي يمكن ان يتغير فيها المحتوى بشكل ديناميكي، على سبيل المثال وضع مربع تسمية (Label) بجوار مربع النص (TextBox) ، قد يؤدي إلى التداخل بينهما إذا تغير مربع التسمية بحيث صار محتويا على نص اطول مما كان متوقعاً. ولسد هذه الفجوة جاء الإصدار الثاني من Windows Form ليقدم الواح مثل FlowLayoutPanel و TableLayoutPanel ، يمكن من خلالها إنشاء نوافذ متطورة ومشابهة لحد كبير لصفحات الوب. مثل هذه الألواح تسمح للكنترولات بإن تنمو وتكبر وان تزيح الكنترولات الاخرى المجاورة لها بشكل تلقائي. مما سهل علينا التعامل مع المحتوى الديناميكي ، وإنشاء الواجهات المحلية . لكن المشكلة ظلت في ان عملية التخطيط (Layout) تعتمد اساسا على أحداثيات ثابتة يصعب التعامل معها من اجل إنشاء واجهات اكثر مرونة. جاءت WPF لتحل هذه المشكلة ، عن طريق ابتكار نظام تخطيط جديد تأثر كثيرا بالاسلوب السابق في Windows Form ، وقام بتغيير الأسلوب القديم الذي يقوم على اساس الأحداثيات الثابتة ، ليصبح التخطيط قائما على اساس التتبع(flow-based layout) . والنتيجة انه اصبح بامكاننا إنشاء واجهات اكثر تطورا واكثر تعقيدا من السابق، لها ان تعمل بشكل ممتاز مهما كانت دقة الشاشة المستخدمة ، ومهما كان التغيير الحاصل في حجم النافذة . فلسفة التخطيط في WPF: لو نظرنا إلى النوافذ في WPF سوف نجد ان كل نافذة يمكن ان تحتوي على عنصر واحد فقط (كون النوافذ مشتقة من الفئة ContentControl). ومن اجل ان نقوم بإضافة اكثر من عنصر اليها فلا بد اولا من ان نضع فيها احد الألواح ثم نضيف اليه العناصر المطلوبة. التخطيط في WPF بشكل عام يعتمد على هذه الفكرة ، وهي اختيار اللوح المناسب ثم اضافة العناصر اليه. ومن الجيد هنا ان نذكر بعض القواعد الهامة التي يفضل اتباعها عند تصميم الواجهة: - يجب ان لا نحدد حجم العناصر بشكل صريح، وبدلا من هذا فإن كل عنصر يمكنه ان يحدد الحجم المناسب له استنادا على ما يحتويه ، فالزر مثلا يمكن ان يتسع تلقائيا كلما زدنا من طول النص بداخله. - موقع اي عنصر لا يتم تحديده على اساس الاحداثيات (س،ص)، واللوح هو من يقوم بترتيب العناصر وتحديد موضعها في النافذة استنادا على: 1) حجم كل عنصر، 2) ترتيبه داخل اللوح ،3) بعض المعلومات الأخرى التي يتم تزويد اللوح بها من خلال الخصائص المرفقة (Attached Properties). - تقوم الالواح بتقسيم المساحة المتاحة لها على عناصرها الأبناء، حيث تحاول ان تعطي كل عنصر الحجم الذي يناسبه(استنادا على ما يحتويه العنصر). - الألواح يمكن ان تتداخل فيما بينها، حيث ان اي لوح يمكن ان يحتوي بداخله على لوح آخر ويقوم اللوح الأب بالتعامل مع اللوح الأبن بنفس الطريقة التي يتعامل بها مع اي عنصر ابن آخر. عملية التخطيط في WPF تمر عملية التخطيط (Layout) في WPF بمرحلتين هما: مرحلة القياس (measure) ومرحلة الترتيب (arrange) ، في مرحلة القياس يقوم اللوح بالمرور على العناصر بداخله ويسئل كل عنصر عن الحجم الذي يناسبه. وفي مرحلة الترتيب يقوم اللوح بوضع كل عنصر في مكانه المناسب. خلال هذه العملية ليس من الضروري ان يتم اعطاء العنصر الحجم الذي يريده تماما ، فقد لا يكون اللوح كافياً لمثل هذا العنصر وفي هذه الحالة يحدث اقتصاص لبعض أجزاء العنصر. ملاحظة هامة: تذكر ان الالواح لا تدعم عملية التمرير (scrolling). وان هناك عنصر خاص بهذه العملية هو ScrollViewer يمكن استخدامه في اي مكان وداخل اي عنصر آخر. الواح التخطيط جميع الألواح في WPF مشتقة من الفئة المجردة Panel (تقع في المجال System.Windows.Controls)، ولعل الخاصية Children هي اهم خاصية فيها حيث تمثل كولكشن يحتوي على العناصر الأبناء داخل اللوح. اهم الفئات المشتقة من Panel هي: - StackPanel: تقوم بتكديس العناصر بشكل عمودي او افقي. - WrapPanel: تقوم بترتيب العناصر على شكل اسطر، حيث يتم تعبئة السطر الأول ثم الثاني وهكذا. - DockPanel: تقوم برصف العناصر في جانب من النافذة ، وتجعل العنصر الأخير يشغل المساحة المتبقية. - Grid: تقوم بترتيب العناصر على شكل صفوف واعمدة. - UniformGrid: تقوم بترتيب العناصر على صفوف واعمدة ، لكن جميع الخلايا فيها يكون لها نفس الحجم. - Canvas: تسمح بوضع العناصر على اساس الأحدثيات (س،ص) . وهذا اللوح يشبه إلى حد كبير الطريقة المستخدمة في WinForm لكن لا يوجد دعم لمميزات anchoring و docking إضافة إلى ان هناك الواح مخصصة لبعض الكنترولات مثل اللوح TabPanel الذي يستخدم في ترتيب التبويبات داخل TabControl، واللوح ToolbarPanel الذي يستخدم في ترتيب الأزرار في شريط الأدوات (Toolbar) ، وغيرها من الألواح. استخدام StackPanel يعتبراللوح StackPanel من ابسط الألواح على الأطلاق ، حيث يقوم بترتيب وتكديس العناصر بشكل عمودي او افقي . ويتم تحديد اتجاه التكديس عن طريق الخاصية Orientation التي يمكن ان تكون قيمتها: - Vertical *( القيمة الأفتراضية): من اجل ترتيب العناصر بشكل عمودي من الأعلى إلى الأسفل . - Horizontal: من اجل ترتيب العناصر بشكل افقي من اليسار إلى اليمين. مثال1: <Window x:Class="SimpleLayout.Window1" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="مثال بسيط على Layout" Height="300" Width="300"> <StackPanel> <Label>A Button Stack</Label> <Button>Button 1</Button> <Button>Button 2</Button> <Button>Button 3</Button> <Button>Button 4</Button> </StackPanel> </Window> النتيجة: مثال2: نفس المثال السابق مع تغيير اتجاه التكديس. <Window x:Class="SimpleLayout.Window1" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="مثال بسيط على Layout" Height="300" Width="300"> <StackPanel Orientation="Horizontal"> <Label>A Button Stack</Label> <Button>Button 1</Button> <Button>Button 2</Button> <Button>Button 3</Button> <Button>Button 4</Button> </StackPanel> </Window> النتيجة: خصائص العناصر المتعلقة بالتخطيط : رغم ان التخطيط هو من مسئولية اللوح بشكل اساسي إلا ان لكل عنصر بعض الخصائص التي يمكن ان تؤثر خلال هذه العملية. هذه الخصائص هي: - HorizontalAlignment: تحدد الكيفية التي يتم بها وضع العنصر الأبن داخل اللوح عندما تكون له مساحة افقية فائضة . ويمكن ان تأخذ احدى القيم التالية : Left ، Center ، Right او Stretch *. - VerticalAlignment: تحدد الكيفية التي يتم بها وضع العنصر الأبن داخل اللوح عندما تكون له مساحة رأسية فائضة. ويمكن ان تأخذ احدى القيم التالية: Top ، Center ، Bottom او Stretch *. - Margin: تحدد حجم الهامش الذي يجب ان يترك في كل جانب من جوانب العنصر . نوع هذه الخاصية هو Thickness وهو عبارة عن سجل يمكن ان يحتوي على رقم محدد لكل جانب من الجوانب الأربعة (يسار، اعلى، يمين ،اسفل)، القيمة الأفتراضية لهذه الخاصية هو (0, 0، 0، 0) ما يعني ان العنصر ليس له اي هامش(تذكر دائما ان القيمة الأفتراضية لإي خاصية قد تختلف في بعض العناصر). - MinWidth و MinHeight: لتحديد الحد الأدنى لعرض و ارتفاع العنصر. - MaxWidth و MaxHeight: لتحديد الحد الأعلى لعرض و ارتفاع العنصر. - Width و Height: لتحديد عرض وارتفاع العنصر بشكل صريح، بمعنى ان العنصر سوف يظهر بذلك الحجم بغض النظر عن محتواه (هل يكفي ام لا؟ ) وبغض النظر عن خصائص المحاذاة الأفقية او الرئيسية.(لهذا ينصح بعدم استخدام خاصيتي العرض او الأرتفاع او التقليل من استخدامها) جميع هذه الخصائص معرفة في الفئة FrameworkElement والتي تتفرع منها معظم العناصر في واجهة المستخدم ، بما فيها الألواح نفسها. ولكي نفهم هذه الخصائص فلا نبد ان نتناولها بشئ من التفصيل. المحاذاة (Alignment): تحدد خصائص المحاذاة في العنصر اين وكيف يتم وضع العنصر داخل المساحة المعطاه له من قبل اللوح الأب. بالنسبة للمحاذاة الأفقية (HorizontalAlignment) يمكن ان تأخذ قيمة من التالي: - Stretch *: وتعني ان العنصر سوف يتم تمديده ليشغل كل المساحة الممنوحة له. - Left: وضع العنصر في الجانب الأيسر. - Center: توسيط العنصر - Right: وضع العنصر في الجانب الأيمن اما المحاذا الرئيسية (VerticalAlignment) فيمكن ان تكون: - Stretch *: وتعني ان العنصر سوف يتم تمديده ليشغل كل المساحة الممنوحة له. - Top: وضع العنصر في الأعلى - Center: توسيط العنصر - Bottom: وضع العنصر في الأسفل الكود التالي يوضح كيفية التعامل مع المحاذاة الأفقية . مثال3: <Window x:Class="ATS.WPFLayout2.Phase1.L_StackPanel.L_StackPanel3" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="خصائص المحاذاة" Height="300" Width="300"> <StackPanel> <Label HorizontalAlignment="Center">A Button Stack</Label> <Button HorizontalAlignment="Left">Button 1</Button> <Button HorizontalAlignment="Right">Button 2</Button> <Button HorizontalAlignment="Stretch">Button 3</Button> <Button>Button 4</Button> </StackPanel> </Window> النتيجة: عندما نقوم بتغيير حجم النافذة فإن العناصر سوف تظهر بنفس الشكل تلقائيا. فعند تكبير النافذة: وعند تصغير النافذة: لاحظ ايضاً انه لوقمنا بتغيير المحاذاة الرأسية في هذا المثال، فإنه لا يكون لها اي تأثير على العناصر، فعندما يكون اتجاه التكديس رأسي (Orientation تساوي Vertical) في StackPanel، فإنها تقوم بأعطاء كل عنصر الأرتفاع الذي يريده تماما. وبنفس الطريقة ايضاً فإن المحاذاة الأفقية لا يكون لها اي تأثير على العناصر عندما يكون اتجاه التكديس افقي لأن StackPanel تعطي كل عنصر العرض الذي يريده تماماً. الهوامش(Margin) لو عدنا إلى المثال1 ، سوف نجد ان العناصر تلتصق ببعضها البعض، وقد يكون من الأفضل لو اننا استطعنا ان نضع فراغ بسيطا بينها . وهنا يأتي دور الخاصية Margin، فعن طريقها يمكن تحديد الهامش المطلوب لأي جانب من جوانب العنصر الأربعة (يسار، اعلى، يمين ، اسفل) . توجد مرونة كبيرة في تحديد قيمة الخاصية Margin ، حيث يمكننا اعطاء نفس الهامش لكل الجوانب دفعة واحدة بالشكل التالي: <Button Margin="5">Button 3</Button> او تعيين الجوانب الأفقية (left و right) بهامش معين ، والجوانب الرأسية (top و bottom) بهامش آخر : <Button Margin="5,10">Button 3</Button> كما يمكننا اعطاء هامش مختلف لكل جانب وفق الترتيب left، top، right، bottom : <Button Margin="5,10,5,10">Button 3</Button> وتعيين الهوامش من خلال الكود C# يكون بالشكل التالي: button1.Margin = new Thickness(5); //Or button1.Margin = new Thickness(5, 5, 5, 5); تذكر دائما ان الهوامش بين العناصر تكون تراكمية ، فإذا وضعنا الزر 2 اسفل الزر 1 وكان الهامش السفلي للزر رقم 1 يساوي 5 والهامش العلوي للزر 2 ايضاً يساوي 5 فإن الهامش النهائي بين الزرين سيكون مساويا لـ 10. مثال 4: <Window x:Class="SimpleLayout.ElementsMargin" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="الهوامش- Margin" Height="300" Width="300"> <StackPanel Margin="3"> <Button Margin="3">Button 1</Button> <Button Margin="3">Button 2</Button> <Button Margin="3">Button 3</Button> <Button Margin="3">Button 4</Button> </StackPanel> </Window> النتيجة: والشكل التالي يوضح كيف تتراكم الهوامش: خصائص الحجم: كل عنصر في WPF لديه الخاصتين Width و Height والتي تسمح لنا بتحديد عرض وارتفاع العنصر بشكل صريح ومباشر. وكما ذكرنا سابقا ان مثل هذه التحديد غير مفضل في WPF . إذا كان من الضروري تقييد او حصر حجم العنصر فإنه من الأفضل استخدام الخصائص : - MinWidth و MinHeight من اجل تحديد الحد الأدنى لعرض وارتفاع العنصر. - MaxWidth و MaxHeight من اجل تحديد الحد الأعلى لعرض وارتفاع العنصر. مثال5: لنفرض في مثال "المكدس والأزرار" انه قد نقرر ان الأزرار يجب ان تتسع باتساع StackPanel لكن بشرط ان لا يزيد عرضها عن 200 وحدة ولا يقل عن 100 وحدة . <Window x:Class="SimpleLayout.ElementsSize" xmlns="[url="http://schemas.microsoft.com/winfx/2006/xaml/presentation"]http://schemas.microsoft.com/winfx/2006/xaml/presentation[/url]" xmlns:x="[url="http://schemas.microsoft.com/winfx/2006/xaml"]http://schemas.microsoft.com/winfx/2006/xaml[/url]" Title="حجم العناصر" Height="300" Width="300"> <StackPanel Margin="3"> <Label Margin="3" HorizontalAlignment="Center"> A Button Stack</Label> <Button Margin="3" MaxWidth="200" MinWidth="100">Button 1</Button> <Button Margin="3" MaxWidth="200" MinWidth="100">Button 2</Button> <Button Margin="3" MaxWidth="200" MinWidth="100">Button 3</Button> <Button Margin="3" MaxWidth="200" MinWidth="100">Button 4</Button> </StackPanel> </Window> النتيجة: التداخل في الألواح: كما ذكرنا سابقاً انه يمكن لأي لوح ان يحتوي على لوح آخر ، ويقوم اللوح الأب بالتعامل مع اللوح الأبن بنفس الطريقة التي يتعامل بها مع اي عنصر ابن آخر. واستخدام اكثر من لوح هو امر شائع في WPF من اجل إنشاء التخطيط المطلوب، فعلى سبيل المثال لو اردنا إنشاء مربع حوار يتكون من زرين موافق والغاء الأمر يكونان في الأسفل ومربع نص يشغل المساحة المتبقية في النافذة فإننا نستخدم: <DockPanel LastChildFill="True"> <StackPanel DockPanel.Dock="Bottom" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Margin="10,10,2,10" Padding="3">OK</Button> <Button Margin="2,10,10,10" Padding="3">Cancel</Button> </StackPanel> <TextBox DockPanel.Dock="Top" Margin="10">This is a test.</TextBox> </DockPanel> النتيجة استخدام Grid: يعتبر هذا اللوح من افضل الألواح على الأطلاق ويمكننا بواسطته عمل كل شئ يقوم به اي لوح آخر(تقريباً). يقوم هذا اللوح بتقسيم المساحة المعطاة له إلى صفوف واعمدة غير مرئية ، ويمكن وضع العناصر داخل الخلايا (الناتجة من تقاطع الصف مع العمود) . والجميل هنا انه يمكن وضع اكثر من عنصر في نفس الخلية، وبشكل افتراضي كل Grid يتكون من صف واحد وعمود واحد ، ما يعني بانه يتكون من خلية واحدة بشكل افتراضي. من اجل اضافة الصفوف و الأعمدة إلى Grid ، فإن علينا استخدام الخاصيتين Grid.RowDefinitions و Grid.ColumnDefinitions ، فلو اردنا اضافة صفين وثلاثة اعمدة فإننا نستخدم: <Grid> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> <ColumnDefinition/> <ColumnDefinition/> </Grid.ColumnDefinitions> </Grid> تكون الصفوف متساوية في الأرتفاع ، والأعمدة متساوية في العرض بشكل افتراضي. ملاحظة: لاحظ انه يجب استخدام العنصر Grid.RowDefinitions ، والعنصر Grid.ColumnDefinitions من اجل تحديد الصفوف والأعمدة المطلوبة. تسمى هذه الصيغة في Xaml بـ "صيغة العنصر الخاصية" (Property Element Syntax) وهي طريقة اخرى لتعيين الخصائص في الكائن إلى جانب "صيغة الواصفة" (Attribute Syntax) . بعد ذلك يمكننا إضافة العناصر إلى Grid وتحديد الصف والعمود الذي يقع فيه كل عنصر عن طريق الخصائص المرفقة Grid.Row و Grid.Column: <Grid> ……… <Button Grid.Row="0" Grid.Column="0">Top Left</Button> <Button Grid.Row="0" Grid.Column="1">Middle Left</Button> <Button Grid.Row="1" Grid.Column="2">Bottom Right</Button> <Button Grid.Row="1" Grid.Column="1">Bottom Middle</Button> ……… </Grid> الصفوف والأعمدة في Grid ترقم على الأساس الصفري ، بمعنى ان الصف الأول يكون رقمه 0 والصف الثاني يكون رقمه 1 وهكذا. إذا لم نقم بتحديد الصف او العمود لعنصر معين فإن هذا العنصر يتم وضعه في الصف الأول والعمود الأول ، بسبب ان القيمة الأفتراضية للخصائص المرفقة Grid.Row و Grid.Column هي صفر(بمعنى الأول). حجم الأعمدة والصفوف: تقدم Grid ثلاث طرق لتحديد ارتفاع الصف وعرض الأعمدة: - الحجم المطلق (Absolute size): حيث يتم تعيين رقم يمثل عدد الوحدات التي يشغلها العرض او الأرتفاع <ColumnDefinition Width="100"/> - الحجم التلقائي(Automatic size): حيث يعطى الصف او العمود القدر الذي يحتاج اليه تماما (استنادا على حجم المحتوى) <ColumnDefinition Width="Auto"/> - الحجم النسبي(Proportional size): حيث يتم تقسيم المساحة المعطاة على الصفوف اوالأعمدة <ColumnDefinition Width="*"/> وبشكل افتراضي، يكون التناسب متساويا ما لم نقم بإضافة رقم معين يسبق النجمة مثل: <RowDefinition Height="*"/> <RowDefinition Height="2*"/> بهذا تكون نسبة الصف الأول إلى الصف الثاني هي 1 إلى 2. عمل مربع الحوار السابق باستخدام Grid: باستخدام هذه الطرق في التحجيم فإنه يمكننا استخدام Grid من اجل إنشاء نفس مربع الحوار السابق على النحو التالي : <Grid ShowGridLines="True"> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> </Grid.RowDefinitions> <TextBox Margin="10" Grid.Row="0">This is a test.</TextBox> <StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Margin="10,10,2,10" Padding="3">OK</Button> <Button Margin="2,10,10,10" Padding="3">Cancel</Button> </StackPanel> </Grid> الخاصية ShowGridLines: يمكن عرض خطوط الشبكة في Grid بتعيين هذه الخاصية إلى True (كما يظهر في المثال السابق). الخصائص المرفقة Grid.RowSpan و Grid.ColumnSpan يمكننا ان نجعل العنصر يشغل اكثر من صف واحد او عمود واحد من خلال الخصائص Grid.RowSpan و Grid.ColumnSpan، فعلى سبيل المثال لو اردنا ان يكون الزر في الصف الأول والعمود الأول ويشغل صفين وعمودين فإننا نكتب : <Button Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" Grid.ColumnSpan="2">Span Button</Button> تقسيم النوافذ باستخدام GridSplitter باستخدام GridSplitter يمكننا ان نجعل المستخدم قادرا على تغيير عرض العمود او ارتفاع الصف بنفس الطريقة التي تظهر في نوافذ مثل Windows Explorer حيث يكون بالأمكان تغيير عرض قائمة المجلدات عن طريق سحب حافتها في الأتجاه المطلوب. إضافة GridSplitter إلى Grid عملية سهلة رغم ان الطريقة غريبة بعض الشئ، ومن الجيد ان نتذكر التالي: - يجب وضع GridSplitter داخل خلية في Grid ، هذه الخلية يمكن ان تحتوي على عناصر اخرى وفي هذه الحالة يجب ان نتأكد من هذه العناصر لا تقوم بإخفاء GridSplitter (بمعنى تغطيه) عن طريق تعيين الهوامش (Margin). والأفضل ان نقوم بعمل عمود او صف مخصص لـ GridSplitter - لا يقوم GridSplitter بتعديل حجم الخلية فقط انما يقوم بتعديل حجم الصف او العمود بالكامل، ولكي نجعل هذا السلوك ينسجم مع المظهر في UI فإن علينا ان نجعل GridSplitter يمتد على طول الصف او العمود المطلوب باستخدام الخصائص Grid.RowSpan و Grid.ColumnSpan. - مبدأيا يكون GridSplitter غير مرئيا ، ومن اجل ان نجعله مرئيا فلا بد من اعطائه حجم معين ينسجم مع الأتجاه الذي يعمل عليه. عن طريق اعطائه عرض معين (مع الأعمدة) او طول معين (مع الصفوف). - تحدد محاذاة GridSplitter طريقة عمله ، بمعنى هل سيعمل كمقسم افقي (لتغيير حجم الصفوف) او كمقسم رأسي (لتغيير حجم الأعمدة) ، فمن اجل ان يعمل كمقسم افقي فإنه يجب تعيين الخاصية VerticalAlignment إلى Center بحيث يتم تغيير حجم الصف في الأعلى والصف في الأسفل. ومن اجل ان يعمل كمقسم رأسي فإنه يجب تعيين الخاصية HorizontalAlignment إلى Center بحيث يتم تغيير حجم العمود في اليسار والعمود في اليمين. (يمكن ايضاً استخدام الخصائص ResizeDirection و ResizeBehavior في GridSplitter من اجل تحديد السلوك المطلوب) المثال التالي يوضح كيفية عمل مقسم رأسي لتغيير حجم الأعمدة : <Grid> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth="100"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition MinWidth="50"></ColumnDefinition> </Grid.ColumnDefinitions> <Button Grid.Row="0" Grid.Column="0" Margin="3">Left</Button> <Button Grid.Row="0" Grid.Column="2" Margin="3">Right</Button> <Button Grid.Row="1" Grid.Column="0" Margin="3">Left</Button> <Button Grid.Row="1" Grid.Column="2" Margin="3">Right</Button> <GridSplitter Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Width="3" VerticalAlignment="Stretch" HorizontalAlignment="Center" ShowsPreview="False"></GridSplitter> </Grid> الخاصية ShowsPreview : تكون قيمة الخاصية ShowsPreview في GridSplitter هي False بشكل افتراضي ، مايعني ان GridSplitter سوف يقوم بتغيير حجم العمود او الصف مباشرة خلال عملية السحب، وعند تغيير هذه الخاصية إلى True فإن GridSplitter سوف يسمح للمستخدم ان يقوم بمعاينة الحجم المحدد اولا وعند تحرير زر الماوس فإن الحجم الجديد يتم تطبيقه. سوف احاول إن شاء الله ان اضع الجزء الثاني المكمل في اقرب وقت.
  11. نحتاج فريق للتوثيق،

    مبدأ المسئولية الواحدة (SRP) SRP.rar
  12. كيف انشاء Delegate و Event وربطها مع بعض

    شرح الكائن Delegate وكيفية الاستفادة منه في البرمجة المتقدمة
  13. ماهي تقنية Sliver Light ؟؟؟

    تحتاج تحمل : Microsoft Visual Studio 2008 Service Pack 1 او على الأقل .NET Framework 3.5 SP1 وبالمناسبة البرامج في Silverlight 3 ممكن تشتغل بدون متصفح. http://www.sdtimes.com/link/33355
  14. مبدأ الانفتاح والانغلاق The Open-Closed Principle - OCP مكونات البرنامج (الفئات ،الوحدات، الدوال ...الخ) يجب ان تكون منفتحة على التوسيع ومنغلقة على التعديل (Bertrand Meyer) إذا كان التغيير الواحد في البرنامج يعني التغيير في العديد من أجزاءه الأخرى، فإن تصميمه يكون جامداً. مبدأ OCP ينصحنا بأن نقوم بتحسين التصميم ، بحيث ان مثل هذه التغيرات لن تحتاج إلى الكثير من التعديلات مستقبلاً. بل الواقع انه إذا تم تطبيق مبدأ OCP بشكل جيد ، فإن اي تغيير يمكن إنجازه عن طريق إضافة كود جديد بدون تعديل الكود القديم. قد يبدو هذا صعبا إلى حد ما، لكن هناك بعض الاستراتيجيات الفعالة والبسيطة نسبياً والتي يمكن ان تقربنا من هذا الهدف. وصف مبدأ OCP لأي وحدة (module) تتفق مع مبدأ OCP خاصيتان: - مفتوحة على التوسيع: فكلما تغيرت المتطلبات ، كان بإمكاننا ان نوسع الوحدة بالسلوكيات الجديدة التي تلبي تلك المتطلبات. - منغلقة على التعديل: فتوسيع سلوك الوحدة لن يؤثر على الكود المصدري او الملف الثنائي الخاص بها. قد يبدو ان الخاصيتين تناقض احداها الأخرى ، فالطريقة الطبيعية لتوسيع الوحدة هي عن طريق تغيير الكود الخاص بها، كذلك فإننا نعتقد ان الوحدة التي لا تتغير، لن يتغير سلوكها ايضاً. إذن كيف يكون بالإمكان تعديل سلوك الوحدات بدون تعديل الكود المصدر لها؟ وبدون تعديل الوحدة كيف يمكننا تعديل ما تقوم به؟ الجواب يكمن في التجريد، ففي أي لغة كائنية التوجه مثل C# اوغيرها ، يكون بالإمكان تعريف تجريدات ثابتة تمثل مجموعة من السلوكيات التي لا تزال غير محددة حتى الآن ، وهذه التجريدات عبارة عن فئات مجردة (abstract classes) ، يمكن أشتقاق فئات فرعية منها لتمثيل تلك السلوكيات.كما يمكننا تعريف الوحدات التي تقوم بمعالجة تلك التجريدات ، ومثل هذه الوحدات يمكن ان تكون منغلقة على التعديل حيث انها تعتمد على تجريدات ثابتة لا تتغير (او نادرا ما تتغير)، كذلك فإن سلوكها قابل للتمدد عن طريق الاشتقاق من تلك التجريدات. الشكل التالي يعرض تصميم بسيط لا يتفق مع مبدأ OCP . حيث ان كلا الفئتين الزبون (Client) و الخادم (Server) هي فئات مادية (concrete= قابلة للتمثيل). وبما ان فئة الزبون تستخدم فئة الخادم مباشرة ، فإن علينا ان نقوم بالتعديل فيها في كل مرة نريد لها ان تستخدم خادم آخر، وهذا يخالف مبدأ OCP. ويبين الشكل التالي التصميم المكافئ الذي يتفق مع مبدأ OCP باستخدام "اسلوب التصميم: الإستراتيجية" (Strategy pattern). حيث قمنا بتعريف الواجهة المجردة ClientInterface وجعلنا فئة الزبون تتعامل مع ذلك التجريد. والنتيجة انه يمكن للزبون ان يتعامل مع كائنات فئة الخادم او اي فئة أخرى مشتقة من ClientInterface، بدون الحاجة إلى تعديل فئة الزبون. يعرض الشكل التالي خيارا آخر بأسلوب الدالة القالب (Template method pattern) ، ففئة البوليصة لديها بعض الدوال المادية (concrete functions) والعامة التي تمثل البوليصة بنفس الطريقة مع فئة الزبون ، وبنفس الطريقة ايضاً، فإن البوليصة تعرف بعض الوظائف التي تحتاج إليها على شكل واجهة مجردة (لاحظ ServiceFunction). لكن هذه المرة فإن الواجهة المجردة هي جزء من فئة البوليصة نفسها وتكون في لغة مثل C# على شكل دوال مجردة (abstract methods) يتم تحقيقها بواسطة الفئات الفرعية ، لهذا فإن السلوك المحدد داخل البوليصة يمكن ان يتمدد او يعدل من خلال هذه الفئات الفرعية. هذين الأسلوبين هما أشهر طريقتين لإرضاء مبدأ OCP ، حيث يمثل كل اسلوب طريقة واضحة لفصل الدوال عن تفاصيل وأسلوب تحقيقها. برنامج الأشكال تخيل انه مطلوب منا عمل برنامج يكون قادرا على رسم الدوائر والمربعات في بيئة رسوميات معينة. الدوائر والمربعات يجب ان ترسم وفق ترتيب معين. بحيث يتم إنشاء قائمة من الدوائر والمربعات وفق اي ترتيب معين ، وعلى البرنامج ان يقوم بالمرور على القائمة ويرسم كل دائرة او مربع بداخلها. حل يخرق مبدأ OCP: يعرض الكود التالي حل لمثل هذه المشكلة ، حيث قمنا بتعريف سجل لتمثيل الدائرة وسجل آخر لتمثيل المربع ، ايضاً قمنا بعمل الدالة DrawAllShapes التي تقوم بالمرور على قائمة من الأشكال وتفحص نوع كل عنصر فيها ثم تقوم باستدعاء دالة الرسم المناسبة (DrawSquare او DrawCircle). public struct Circle { public double itsRadius; public Point itsCenter; } public struct Square { public double itsSide; public Point itsTopLeft; } public void DrawAllShapes(ArrayList list) { foreach (object s in list) { if (s is Square) { DrawSquare((Square)s); } else if (s is Circle) { DrawCircle((Circle)s); } } } public void DrawCircle(Circle c) { } public void DrawSquare(Square s) { } لاحظ ان الدالة DrawAllShapes لا تتفق مع مبدأ OCP لأنها غير منغلقة على الأشكال الجديدة التي يمكن إضافتها لاحقاً، فعلى سبيل المثال إذا أردنا لها ان تكون قادرة على رسم قائمة من الأشكال التي تتضمن المثلث ، فالسبيل الوحيد لتحقيق ذلك هو عن طريق التعديل فيها. ونفس الشئ سوف يتكرر كلما أردنا ان نقوم بإضافة شكل جديد آخر إلى التطبيق. بالتأكيد، هذا البرنامج ما هو إلا مثال بسيط ، بينما البرامج الحقيقية ستحوي الكثير من الدوال التي تقوم كل منها بوظيفة معينة متعلقة بالشكل مثل دوال التحريك ، التكبير ، الحذف ، النسخ ، اللصق...الخ ، وسوف نجد ان عبارة if/else التي استخدمناها في DrawAllShapes قد كتبت بطريقة مشابهه في كثير من تلك الدوال. هذا يعني ان إضافة شكل جديد يتطلب منا ان نبحث عن تلك الأماكن وان نقوم بإضافة الكود الخاص بالشكل الجديد. والعملية لا تتم بهذه البساطة، فغالبا ان عبارات if/else او switch لن تكون بالشكل البسيط الموضح في DrawAllShapes ، فالشروط يمكن ان تكون مركبة ومحتوية على بعض العوامل الأخرى (منطقية ، حسابية ...الخ) ، ايضاً قد نجد ان هناك دوال يمكن تنفيذها على المربع و الدائرة بنفس الطريقة وبالتالي فإنها لا تحتاج إلى عبارات if/else او switch. ما يعني إن عملية إيجاد وفهم كل الأماكن التي يجب ان نضيف إليها الشكل الجديد ليست سهلة إطلاقا. وحتى اذا استطعنا ان نغير في كل عبارات if/else، فالمسئلة لا تنتهي عند هذا الحد، فعلينا ان نقوم بإعادة بناء وتحزيم ونشر البرنامج مرة أخرى ، ويمكنك ان تتخيل مقدار الوقت والجهد المطلوب لذلك. لاحظ ان مجرد إضافة الشكل الجديد إلى البرنامج قد أدى إلى سلسة من التغيرات المتتالية على الكود وملفات .exe او .dll ...الخ، لهذا فإن الثمن الذي سندفعه من اجل القيام بهذه العملية قد يكون مرتفعا إلى أقصى الحدود. مما سبق يمكننا القول ان مثل هذا الحل يعتبر: - جامد (rigid) لأنه يصعب التغيير فيه. - هش (fragile) لأنه قابل للكسر بصورة غير متوقعة – تخيل ماذا سيحدث لو أننا نسينا التغيير في جزء يستخدم عبارة if؟. - ساكن (immobile) بمعنى انه غير قابل لإعادة الاستخدام بسهولة – فلا يمكن استخدام الدالة DrawAllShapes بدون ان نحضر معها المربع والدائرة حل يتفق مع OCP : الكود التالي يعرض الحل الذي يتلاءم مع مبدأ OCP لنفس مشكلة المربع والدائرة ، وفيه قمنا بإنشاء الواجهة Shape تحتوي على الدالة Draw ، تقوم فئة المربع وفئة الدائرة بتحقيقها. public interface Shape { void Draw(); } public class Square : Shape { public void Draw() { //draw a square } } public class Circle : Shape { public void Draw() { //draw a circle } } public void DrawAllShapes(IList shapes) { foreach (Shape shape in shapes) shape.Draw(); } من خلال هذا الحل ، وعندما تأتي المتطلبات الخاصة بإضافة المثلث ، فما علينا إلا ان نضيف فئة خاصة بالشكل الجديد تحقق الواجهة Shape. بدون الحاجة إلى تعديل الدالة DrawAllShapes بأي شكل من الأشكال. لهذا فإن الدالة تتلاءم مع مبدأ OCP ، حيث ان سلوكها يمكن ان يتسع بدون ان تعدل. الحقيقة ان عملية إضافة المثلث لن تؤثر على اي جزء من أجزاء الكود السابق، قد تكون هناك أجزاء اخرى في البرنامج ينبغي التعديل فيها من اجل التعامل مع الشكل الجديد ، لكن الكود السابق محصن ضد اي تغير من هذا النوع . في البرامج الحقيقية ، ستحتوي الواجهة Shape على الكثير من الدوال، ورغم هذا ما تزال عملية إضافة شكل جديد إلى البرنامج سهلة للغاية ، حيث ان المطلوب هو اشتقاق فئة فرعية تحقق كل تلك الدوال. بدون الحاجة إلى البحث عن أجزاء النظام التي تحتاج إلى التعديل. وبالتالي فإن الحل ليس بالهش ابدا. ايضاً هذا التصميم ليس بالجامد ، فلا حاجة إلى تعديل الكود او إعادة بناء وترجمة اي وحدة من الوحدات السابقة. واخيراً، فإن هذا الحل ليس ساكن ، فيمكن إعادة استخدام DrawAllShapes بواسطة اي تطبيق بدون الحاجة إلى احضار المربع او الدائرة . بمعنى إن هذا الحل لا يبدي اي علامة من علامات التصميم السئ الذي ذكرناها سابقاً. ومثل هذا البرنامج في مجمله يتلائم مع مبدأ OCP ، حيث انه يتغير عن طريق إضافة كود جديد، بدلا من تعديل الكود الموجود. ويغنينا من القيام بسلسلة التغيرات التي يفرضها البرنامج الآخر الذي لا يتلائم مع مبدأ OCP . والمطلوب يتمثل في إضافة فئة جديدة والقيام ببعض التغيرات اللازمة والمتعلقة بـالدالة main من اجل إنشاء الكائنات الجديدة. كل شئ جيد حتى الآن، لكن ماذا سيحدث على الدالة DrawAllShapes في الحل الأخير، إذا تقرر بان الدوائر يجب ان ترسم قبل المربعات. هذه الدالة ليست منغلقة على مثل هذا التغيير. ولا بد من التعديل عليها بحيث يتم تحقيق ذلك السلوك المطلوب. يقودنا هذا إلى استناج هام، فبشكل عام ومهما كان انغلاق الوحدة فلا بد من وجود بعض التغييرات التي تكون غير منغلقة عليها. فالوحدة المنغلقة بشكل كامل ليس لها وجود. و لإن الأنغلاق لا يمكن ان يكون كاملاً، فيجب ان يكون مخطط له . إذ ان على المصممين ان يقرروا ما هي التغيرات التي يجب ان يكون التصميم منغلق عليها ، وان يستنتجوا انواع التغيرات التي تحدث دائما ، ثم بعد ذلك عليهم بناء التجريدات اللازمة للحماية منها. وهذا يتطلب مقدارا معينا من بصيرة وخبرة المصميمين الذين يأملون بإنهم يعرفون المستخدمين والظروف المحيطة بهم بالشكل الكافي للحكم على ارجحية التغيرات المتنوعة. ثم يقومون بعد ذلك بتطبيق مبدأ OCP في مواجهة اكثر التغيرات احتمالاً. بهذا نعرف ان ثمن الملائمة مع مبدأ OCP مرتفع ، فالتجريد يحتاج إلى مزيد من الوقت والجهد، كما انه يزيد من تعقيد البرنامج . ومما لا شك فيه ان هناك حد اقصى لما يمكن ان يتحمله المطورين. في النهاية سنستنج انه من الأفضل ان نطبق OCP فقط على التغيرات المحتملة فعلا. لكن كيف نعرف ما هي التغيرات المحتملة فعلاً؟ الجواب ان علينا القيام بالبحث المناسب ،ان نسئل الأسئلة المناسبة وان نستخدم خبرتنا وحواسنا المعتادة، وبعد كل ذلك فإننا ننتظر حصول التغيرات! قديما ، كنا نقوم بعمل الكثير من التجريدات ، معتقدين بإنها تمثل خطاطيف يمكن ان تحمينا من التغيرات التي نفكر في حدوثها، كان هذا يشعرنا بإن برامجنا سوف تكون اكثر مرونة. لكن للاسف اننا وضعنا الخطاطيف في الأماكن غير المناسبة ، والأسؤ اننا زدنا من تعقيد البرامج ، وزدنا من العبئ اللازم لصيانتها وتوفير الدعم لها رغم عدم استخدامنا لها اصلاً. وبلا شك ان هذا ليس بالجيد ابدا. فنحن لا نريد ان نحمل برامجنا بالكثير من التجريدات ، بل ان ننتظر حتى نحتاج إليها ثم نقوم بعد ذلك بوضعها. الفكرة هنا ان نطبق المثل الذي يقول: "Fool me once, shame on you. Fool me twice, shame on me." "ان تخدعني مرة فقط فهذا عار عليك ، ان تخدعني مرتين فهذا عار علي" بمعنى ان نقوم بكتابة الأكود مفترضين بإنها لن تتغير ، وعندما تحدث التغيرات فعلا ، فإننا نقوم بعمل التجريدات التي سوف تحمينا منها مستقبلا. اي ان علينا ان نأخذ الرصاصة الأولى ، ثم نقوم بحماية انفسنا من اي رصاصة اخرى مشابهة يمكن ان تنطلق من نفس البندقية. وطالما قررنا ان نأخذ الرصاصة الأولى ، فإن من مصلحتنا ان نأخذ الرصاصة التي تأتي باكراً وبشكل متكرر، فنحن نريد ان نعرف ما هي انواع التغيرات المحتملة قبل ان نذهب بعيدا في مسار عملية التطوير. فكلما طال انتظارنا ، كلما زادت صعوبة إنشاء التجريدات المناسبة. لهذا قد نرغب على تحفيز التغيرات ، من خلال استخدام طرق مثل: - كتابة الأختبارات اولاً ، حيث ان الأختبار يعتبر شكل من اشكال استعمال البرنامج ، وبكتابة الأختبارت اولاً نحن نجبر النظام على ان يكون قابل للاختبار ، والتغيير في قابلية الأختبار لن تدهشنا لاحقاً، فلدينا التجريدات التي وضعناها في النظام لهذا الغرض ، وغالباً ما سوف نجد بان كثير من هذه التجريدات قد حمتنا من بعض الأنواع من التغيرات لاحقاً(قد ترغب في ان تبحث اكثر في هذا الموضوع). - ان نستخدم دورة تطوير قصيرة جدا: ايام بدل الأسابيع - ان نقوم بتطوير الوظائف قبل ان نطور البنية التحتية للبرنامج، وان نقوم دائما بعرضها على المهتمين. - ان نقوم باطلاق البرمجيات باكرا، بحيث نجعلها امام عملائنا ومستخدمينا باسرع ما يمكن استخدام التجريد من اجل الأنغلاق: بما اننا اخذنا الرصاصة الأولى المتمثلة في في رغبة المستخدم في ان ترسم الدوائر قبل المربعات، فإننا نريد الآن ان نحمي انفسنا من تغيرات مستقبلية من هذا النوع. كيف يمكننا ان نغلق الدالة DrawAllShapes على التغيير المتعلق بترتيب الأشكال؟ هنا يجب ان نتذكر دائما ان الأنغلاق يقوم اساساً على التجريد، لذا فإن علينا ان نقوم بعمل "تجريد للترتيب" يحمينا من اي طريقة ترتيب يتفق عليها. ان خطة الترتيب تقضي بانه لو كان لدينا كائنين ، فإنه بالإمكان تحديد ايهما يجب ان يرسم اولا. تقدم لغة C# مثل هذا التجريد من خلال الواجهة IComparable التي تحتوي على دالة واحدة فقط CompareTo . هذه الدالة تأخذ وسيطا واحد ، وتعيد القيمة -1 إذا كان الكائن المستدعى اقل من الكائن الوسيط، و القيمة 0 إذا كان الكائن المستدعى يساوي الكائن الوسيط، والقيمة 1 إذا كان الكائن المستدعى اكبر من الكائن الوسيط. الكود التالي يبين شكل الواجهة Shape عندما ترث IComparable public interface Shape : IComparable { void Draw(); } يعطينا هذا معنى لترتيب الأشكال ومعنى لرسمها وفق الترتيب المناسب ، لكن عملية التجريد لم تنتهي حتى الآن ، إذ ان على كل فئة فرعية من الشكل ان تحقق الدالة CompareTo من اجل تحديد الترتيب المطلوب. اذن كيف يكون هذا، وما هو نوع الكود الذي قد نكتبه في Circle.CompareTo للتأكد من ان الدوائر سوف تأتي قبل المربعات؟ تأمل التالي: public class Circle : Shape { public int CompareTo(object o) { if(o is Square) return -1; else return 0; } } يجب ان يكون جلياً ان هذه الدالة وكل الدوال الأخرى المقابلة في بقية الأشكال، لا تتلائم مع مبدأ OCP . ولا توجد طريقة لكي نجعلها منغلقة على الأشكال الجديدة المشتقة. وفي كل مرة نقوم بإنشاء شكل جديد فإن كل دوال CompareTo سوف تحتاج إلى التغيير. بالطبع، لن يكون لهذا اي قيمة إذا لم يكن هناك اشتقاق لأشكال جديدة ، لكن في المقابل ، إذا كان علينا إنشاء اشكال جديدة بشكل متكرر فإن هذا سوف يكون متعب إلى اقصى الحدود ، ومرة اخرى نكون قد اخذنا الرصاصة الأولى وعلينا نحمي انفسنا من مثيلاتها. استخدام البيانات لتحقيق الأنغلاق: إذا كان علينا ان نغلق الأشكال المشتقة من ان تعرف بعضها البعض، فإنه بامكاننا ان نضع بيانات الترتيب داخل Hashtable ونستخدمها على الوجه التالي: public class ShapeComparer : IComparer { private static Hashtable priorities = new Hashtable(); static ShapeComparer() { priorities.Add(typeof(Circle), 1); priorities.Add(typeof(Square), 2); } private int PriorityFor(Type type) { if(priorities.Contains(type)) return (int)priorities[type]; else return 0; } public int Compare(object o1, object o2) { int priority1 = PriorityFor(o1.GetType()); int priority2 = PriorityFor(o2.GetType()); return priority1.CompareTo(priority2); } } public void DrawAllShapes(ArrayList shapes) { shapes.Sort(new ShapeComparer()); foreach(Shape shape in shapes) shape.Draw(); } بهذا الأسلوب نكون قد نجحنا في اغلاق الدالة DrawAllShapes على مسئلة الترتيب بشكل عام ، وسوف يعمل البرنامج سواء قمنا بإضافة اشكال جديدة او تغيرت خطة الترتيب فيما بعد( كأن يفرض المستخدم ان المربعات يجب تأتي اولاً). ان الشئ الوحيد الغير منغلق على مسئلة ترتيب الأشكال المتنوعة هو ShapeComparer والذي يمكن وضعه داخل وحدة خاصة به (اسمبلي في هذه الحالة) وبشكل منفصل عن بقية الوحدات، بحيث ان التغيير فيه لن يؤثر على اي وحدة اخرى. في الختام: بشكل او بآخر يعتبر مبدأ الأنغلاق والأنفتاح (OCP) جوهر التصميم بالكائنات الموجهة OO. والأمتثال لهذا المبدأ تنتج الفوائد المرجوة من تلك التقنية OO وهي: المرونة، قابلية إعادة الأستخدام، وقابلية الصيانة. كذلك فإن الأمتثال لهذا المبدأ لا يتم ببساطة عن طريق استخدام احدى لغات البرمجة كائنية التوجه . وليس من الجيد ان نقوم بعمل التجريدات على كل جزء من اجزاء البرنامج . عوضا عن هذا علينا ان نقوم بعمل التجريدات لتلك الأجزاء في البرنامج التي تتغير بشكل متكرر . فمقاومة التجريدات السابق لأوانها لا يقل اهمية عن عمل التجريدات نفسها. ================= من كتاب : Martin C. Robert (2006) Agile Principles, Patterns, and Practices in C#. Prentice Hall روابط متعلقة: - The Open-Closed Principle - Open/closed principle - Wikipedia
  15. مبدأ المسئولية الواحدة The Single Responsibility Principle - SRP يجب ان يكون هناك سبب واحد فقط لتغيير الكلاس عندما نقوم بتصميم الفئات في النظام ، فإننا نقوم بتوزيع المسئوليات ، وعلينا خلال هذه المرحلة ان لا نضع اكثر من مسئولية في نفس الفئة لأن كل مسئولية تعتبر محورا من محاور التغيير . فإذا تغيرت المتطلبات فيما بعد ، فإن علينا ان ان نقوم بتغيير المسئولية في الفئة ، وبالتالي فإن الفئة التي تكون لها اكثر من مسئولية سوف يكون لها اكثر من سبب للتغيير. إضافة إلى ذلك فإن المسئوليات داخل الفئة الواحدة يمكن ان تقترن فيما بينها ، وتعديل فئة من اجل مسئولية معينة يمكن ان يفسد قدرة الفئة على تحقيق بقية المسئوليات. للتوضيح ، تأمل في المثال التالي: حيث قمنا بتعريف فئة المستطيل تحتوي على الدالة Area() لحساب مساحة المستطيل ، والدالة Draw لرسم المستطيل على الشاشة . ايضاً قمنا باستخدام تلك الفئة في تطبيقين مختلفين ، الأول ComputationalGeometryApplication يقوم بالحسابات الهندسية ويستخدم فئة المستطيل لهذا الغرض ، ولا يقوم اطلاقاً باي عملية رسم على الشاشة. والتطبيق الآخر GraphicalApplication يقوم ايضاً ببعض الحسابات الهندسية لكنه اساساً مهتم برسم المستطيل على الشاشة. واضح ان مثل هذا التصميم يخرق مبدأ SRP فالمستطيل له مسئوليتان الأولى القيام بالحسابات الهندسية ، والأخرى القيام بعمليات الرسم. ومثل هذا الخرق يؤدي إلى العديد من المشاكل ، اولها انه لكي يقوم التطبيق ComputationalGeometryApplication باستيراد مكتبة المستطيل فإنه يجب ان يقوم ايضاً باستيراد المكتبة GUI حتى وان لم يستخدمها. المشكلة الثانية ، انه إذا تغيرات المتطلبات في GraphicalApplication بالشكل الذي يتوجب معه تغيير فئة المستطيل فإن علينا ان نقوم ايضاً بإعادة بناء واختبار وتحزيم ComputationalGeometryApplication حتى نضمن انه سوف يعمل بالشكل السليم. التصميم الجيد يكون عن طريق فصل المسئوليتين في فئتين مختلفتين كلياً كما هو موضح في الشكل التالي: بهذا لم يعد ComputationalGeometryApplication بحاجة إلى استيراد GUI ولن يتأثر بالتعيرات المتعلقة بالرسم. تعريف المسئولية: المسئولية هي السبب في التغيير تعرف المسئولية في سياق مبدأ SRP، بإنها السبب في التغيير ، فإذا كنت تعتقد ان هناك اكثر من حافز لتغيير الفئة، فإن لتلك الفئة اكثر من مسئولية. احيانا يكون من الصعب رؤية هذا ، فنحن معتادون على مزج المسئوليات والتعامل معها على شكل مجموعات ، على سبيل المثال تأمل في واجهة المودم التالية: public interface Modem { public void Dial(string pno); public void Hangup(); public void Send(char c); public char Recv(); } البعض قد ينظر اليها على انها واجهة ممتازة فجميع الدوال فعلا تنتمي إلى المودم . في الحقيقة ان هذا التصميم يعرض مسئوليتين الأولى هي إدارة الأتصال (Dial و Hangup من اجل فتج واغلاق الأتصال) ، والثانية هي تراسل البيانات (Send و Recv من اجل استقبال وارسال البيانات). والسؤال الذي يطرح نفسه هنا، هل يجب فصل المسئوليتين؟ إجابة هذا السؤال تتوقف على الكيفية التي يمكن ان يتغير بها التطبيق. فإذا كان التطبيق يمكن ان ان يتغير بالشكل الذي يؤثر على توقيع الدوال المتعلقة بإدارة الأتصال ، فإن هذا سوف يؤثر على الزبائن المهتمة بتراسل البيانات ايضاً. في هذه الحالة يجب الفصل بين المسئوليتين كما يوضح الشكل التالي: من ناحية اخرى إذا كان تغيير في مسئولية يعني التغيير في الأخرى ، فلا حاجة إلى فصل المسئوليتين ، حيث ان مثل هذا الفصل لن يؤدي إلا إلى مزيد من التعقيد. فصل المسئوليات المقترنة: لو عدنا إلى الشكل الأخير، سوف تجد اننا وضعنا المسئوليتين في الفئة ModemImplementation ، ورغم ان هذا غير مستحب ، إلا انه ضروري احيانا ، فهناك العديد من العوامل المتعلقة بتفاصيل العتاد ونظام التشغيل والتي تجبرنا على القيام بمثل هذا الأقتران ، رغم هذا وعن طريق فصل الواجهات فإننا قمنا بفك هذا الأقتران بالدرجة المطلوبة تماماً ، وعليه فإن ModemImplementation هو عنصر مجهول لبقية العناصر لا يتم التعامل معه مباشرة ، باستثناء الدالة main في التطبيق. ======= من كتاب : Martin C. Robert (2006) Agile Principles, Patterns, and Practices in C#. Prentice Hall روابط متعلقة: - SRP: The Single Responsibility Principle - Single responsibility principle - Wikipedia, the free encyclopedia