[ بسم الله الرحمن الرحيم ]
السلام عليكم ورحمة الله.
سنكمل في هذا الدرس ما بدأناه في الدرس الماضي. حيث قمنا في الدرس الماضي بإنشاء قاعدة بيانات جديدة وأنشأنا ضمنها جدولين Category و Product، وصممنا نافذة بسيطة للتعامل مع الجدول Category، كل ذلك تم بالتعامل مع Entity Framework. لذلك سنكمل اليوم التعامل مع الجدول Product، وسنستخدم DataTemplate، لعرض الـ Products من قاعدة البيانات، حيث سنحصل في نهاية المطاف على الشكل التالي:

العمل مشابه للدرس السابق في كثير من الأجزاء، ولكن لا ننسى أن لدينا هنا علاقة بين الجدولين ضاهرة لدينا وهي Product <-> Category. سنلاحظ سهولة التطبيق في هذا الدرس.
لكن دعونا نشرح قليلا عن كود XAML لهذه النافذة. كما قلنا استخدمنا DataTemplate، لعرض البيانات القادمة من قاعدة البيانات. في الواقع الإسم يعطينا انطباع عن ما سيتم عمله، وهو إنشاء قالب معين لعرض البيانات من خلاله. سنمر على الكثير حول هذا الموضوع ضمن درس WPF Templates، ولكن ما اضفناه جوهريا زيادة عن الدرس الماضي هو DataTemplate، لذلك سنشرح عنه بشكل بسيط.
أولاً: البيانات ستعرض ضمن ListBox، أي يجب علينا تعريف هذا القالب ضمنها، كالتالي:
<ListBox>
<ListBox.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>لكن في الواقع WPF أكثر مرونة حيث يمكننا تعريف هذا القالب ضمن الـ Resource مما يمكننا من إعادة استخدامه مرات أخرى، ويكننا من فصل طبقات العمل بشكل أفضل، كالتالي:
<Window.Resources>
<DataTemplate >
...
</DataTemplate>
</Window.Resources>وتزيدنا الـ WPF من جماليتها حيث تعطينا القدرة على تعريف ما يسمى ب Typed Data Template والتي من خلالها نحدد نوع البيانات اللازم عرضه، وستقوم WPF بعدها بعرض تلك البيانات أينما وجدت على أساس هذا القالب، فيصبح لدينا الشكل النهائي التالي:
<Window.Resources>
<DataTemplate xmlns:data="clr-namespace:EntityFrameworkWithWPF" DataType="{x:Type data:Product}">
</DataTemplate>
</Window.Resources>السؤال الأن: ماذا سوف نكتب ضمن هذا القالب؟
في الواقع نستطيع تعريف أي عنصر نريد وبالشكل الذي نريد، لنأخذ مثلاً الشكل في الصورة في الأعلى:

أي أن تصميم القالب كأي تصميم أخر، عبارة عن تعريف مجموعة من العناصر. فيكون كود XAML الكامل للقالب كالتالي:
<Window.Resources>
<DataTemplate xmlns:data="clr-namespace:EntityFrameworkWithWPF" DataType="{x:Type data:Product}">
<Border Name="border" BorderThickness="1,1,1,2" TextBlock.Foreground="DimGray" BorderBrush="DimGray" CornerRadius="1,1,4,4" Width="150" Height="150">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="#FFC4EAF0" Offset="1"/>
<GradientStop Color="#FFF3FBFC" Offset="0.583"/>
</LinearGradientBrush>
</Border.Background>
<Grid Margin="10" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap" HorizontalAlignment="Left" Margin="0,0,0,3">
<Run Text="Name:" FontWeight="Bold"/>
<Run Text="{Binding Path=Name}" />
</TextBlock>
<TextBlock Grid.Row="1" TextWrapping="Wrap" HorizontalAlignment="Left" Margin="0,0,0,3">
<Run Text="Category:" FontWeight="Bold"/>
<Run Text="{Binding Path=Category.Name}" />
</TextBlock>
<TextBlock Grid.Row="2" Margin="0,0,0,3">
<Run Text="Price:" FontWeight="Bold"/>
<Run Text="{Binding Path=Price}"/>
</TextBlock>
<TextBlock Grid.Row="3" Text="Notes:" FontWeight="Bold"/>
<TextBlock Grid.Row="4" Text="{Binding Path=Notes}" TextWrapping="Wrap" Margin="5,0,0,0"/>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>كما نعلم أن ListBox تقوم بعرض عناصر بشكل أسطر متتالية، هذا لانها تحوي على Panel خاصة بترتيب العناصر، وضمن WPF فإن ListBox تحوي على StackPanel افتراضياً، والتي تعرض العناصر فوق بعضها البعض. ولكن ما نريده في مثالنا هو عرض القوالب بشكل مشابة لعرض المجلدات ضمن ويندز. لذلك نحن بحاجة لقولبة Panel الخاصة ب ListBox ووضعها لـ Wrap Panel، والتي تفي بالغرض فهي تعرض العناصر بجانب بعضها البعض وفي كل مرة يتم تكبير النافذة أو تصغيرها فإنا تقوم بإعادة ترتيب العناصر. فيكون لدينا قولبة هذه Panel بالشكل التالي:
<ListBox ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>نلاحظ وضع خاصية [ScrollViewer.HorizontalScrollBarVisibility="Disabled"]، لكي نمنع من امتداد العناصر افقيا، أي كي لا تظهر العناصر بجانب بعضها على سطر واحد وإنما النزول لسطر جديد في حال كثرة العناصر.
أما بالنسبة لتصيميم الواجهة ككل فهو مشابة لما حصل في الدرس الماضي، لذلك سيكون كود XAML الخاص بهذه النافذة كاملاً بالشكل التالي:
<Window x:Class="EntityFrameworkWithWPF.ProductsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Loaded="Window_Loaded"
UseLayoutRounding="True"
WindowStartupLocation="CenterScreen"
Title="ProductsWindow" Height="600" Width="750">
<Window.Resources>
<DataTemplate xmlns:data="clr-namespace:EntityFrameworkWithWPF" DataType="{x:Type data:Product}">
<Border Name="border" BorderThickness="1,1,1,2" TextBlock.Foreground="DimGray" BorderBrush="DimGray" CornerRadius="1,1,4,4" Width="150" Height="150">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White" Offset="0"/>
<GradientStop Color="#FFC4EAF0" Offset="1"/>
<GradientStop Color="#FFF3FBFC" Offset="0.583"/>
</LinearGradientBrush>
</Border.Background>
<Grid Margin="10" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" TextWrapping="Wrap" HorizontalAlignment="Left" Margin="0,0,0,3">
<Run Text="Name:" FontWeight="Bold"/>
<Run Text="{Binding Path=Name}" />
</TextBlock>
<TextBlock Grid.Row="1" TextWrapping="Wrap" HorizontalAlignment="Left" Margin="0,0,0,3">
<Run Text="Category:" FontWeight="Bold"/>
<Run Text="{Binding Path=Category.Name}" />
</TextBlock>
<TextBlock Grid.Row="2" Margin="0,0,0,3">
<Run Text="Price:" FontWeight="Bold"/>
<Run Text="{Binding Path=Price}"/>
</TextBlock>
<TextBlock Grid.Row="3" Text="Notes:" FontWeight="Bold"/>
<TextBlock Grid.Row="4" Text="{Binding Path=Notes}" TextWrapping="Wrap" Margin="5,0,0,0"/>
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Grid Background="WhiteSmoke">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="14" Text="Products" Margin="5"/>
<TextBlock VerticalAlignment="Center" Margin="5"><Hyperlink Name="lnkNew" Click="lnkNew_Click">New</Hyperlink></TextBlock>
</StackPanel>
<ListBox Grid.Row="1" Name="lstProducts" ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionChanged="lstProducts_SelectionChanged">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<Grid Grid.Row="2" Name="gridDetails">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name: " Margin="7"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding Path=Name}" Margin="5"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="Category: " Margin="7"/>
<ComboBox Grid.Row="1" Grid.Column="1" Name="lstCategories" DisplayMemberPath="Name" SelectedItem="{Binding Path=Category}" Margin="5"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Price: " Margin="7"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding Path=Price}" Margin="5"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Notes: " Margin="7"/>
<TextBox Grid.Row="3" Grid.Column="1" AcceptsReturn="True" Text="{Binding Path=Notes}" Height="60" Margin="5"/>
</Grid>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Name="btnAdd" Content="Add" Width="75" Margin="5" Click="btnAdd_Click"/>
<Button Name="btnUpdate" Content="Update" Width="75" Margin="5" Click="btnUpdate_Click"/>
<Button Name="btnDelete" Content="Delete" Width="75" Margin="5" Click="btnDelete_Click"/>
</StackPanel>
</Grid>
</Window>بالنسبة لكود C# لهذه النافذة فلن تجد إختلافاً ابداً بينه وبين ما حصل في الدرس السابق لذلك سيكون لدينا الكود كاملا بالشكل التالي:
public partial class ProductsWindow : Window
{
ShopEntities db = new ShopEntities();
public ProductsWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
db.Products.Load();
lstProducts.ItemsSource = db.Products.Local;
lstCategories.ItemsSource = db.Categories.ToList();
}
// Send the selected item for presentation
private void lstProducts_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
gridDetails.DataContext = lstProducts.SelectedItem;
}
// Create new product object.
private void lnkNew_Click(object sender, RoutedEventArgs e)
{
gridDetails.DataContext = new Product() { Price = 1 };
gridDetails.Children[1].Focus();
}
// Add the product to the database.
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
db.Products.Add((Product)gridDetails.DataContext);
db.SaveChanges();
}
// Update the information of a selected product.
private void btnUpdate_Click(object sender, RoutedEventArgs e)
{
db.SaveChanges();
}
// Delete the selected product object.
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
Product product = (Product)gridDetails.DataContext;
db.Products.Remove(product);
db.SaveChanges();
}
}ملاحظة 1: في الدرس الماضي فاتني ملاحظة هامة فيما يتعلق بتحميل البيانات بواسطة الميثود [;Load()] فهي عبارة عن Extension Method متوضعة ضمن فضاء الأسماء [System.Data.Entity] لذلك من الواجب إضافته.[using System.Data.Entity].
ملاحظة 2: لا نجد في هذا الدرس أو في الدرس السابق أي عمليات تحقق من صحة البيانات، حتى Logic الخاص بالواجهة لا نجده أيضاً، مثلاً: يجب عدم إظهار زر Add للمستخدم إلا في حالة ضغطه على زر New. وأيضاً عدم إظهار زري Update, Delete إلا في حالة قام المستخدم بتحديد عنصر Product من الـ ListBox، ولكن ما يهمنا في هذا الدرس والدرس السابق مقدمة تمهيدية لعرض قوة WPF وجماليتها في صناعة التطبيقات أياً كانت، وسنترك هذه الملاحظة لدرس Validation إن شاء الله تعالى.
ملاحظة 3: يمكننا في تطبيق WPF تحديد النافذة التي تظهر أولا عند تشغيل التطبيق.
فكما هو الحال في Windows Forms عندما كنا نحدد واجهة الإقلاع من program.cs من خلال Application.Run(new Form1());
فإنه يمكننا ذلك في WPF من خلال App.xaml حيث نقوم بوضع خاصية StartupUriإلى إسم النافذة المطلوبة، ففي هذا المثال قمت بتغييرها إلى StartupUri="ProductsWindow.xaml".
تحميل المشروع:
http://www.4shared.com/rar/RflEpuHg/EntityFrameworkWithWPF2.html
إذا كان هناك إي أسئلة أو استفسارات، فأنا موجود إن شاء الله تعالى. بالتوفيق.
