البحث في الموقع
المحتوى عن 'mobile-apps'.
-
سنتناول في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms الجزء الثاني من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسوب. لقد بنينا في الجزء الأوّل تطبيق أساسي أسميناه BasicFeedReader وكانت وظيفته تنحصر في عرض خلاصات قسم مقالات البرمجة. سنطوّر هذه الفكرة من خلال إنشاء تطبيق جديد يعمل على إتاحة الإمكانية للمستخدم بأن يستعرض الخلاصات المتاحة من جميع أقسام الموقع. سيكون ذلك من خلال إضافة صفحة جديدة لعرض أقسام الموقع على شكل مجموعات. لنبدأ فورًا في بناء هذا التطبيق التي سيحتوي على بعض من المكوّنات المتطابقة مع سلفه. بناء تطبيق قارئ الخلاصات المحسّن أنشئ مشروعًا جديدًا من النوع Blank App (Xamarin.Forms Portable) وسمّه EnhancedFeedReader ثم أبق فقط على المشروعين EnhancedFeedReader (Portable) و EnhancedFeedReader.Droid كما وسبق أن فعلنا في هذا الدرس. رغم الشبه الكبير بين هذا التطبيق وسلفه إلًّا أنّني آثرت إعادة إدراج الشيفرة البرمجيّة للأجزاء المتشابهة، وذلك لكي يكون هذا التطبيق متكاملًا قائمًا بحد ذاته. أضف المجلّدين التاليين إلى المشروع EnhancedFeedReader (Portable) (عن طريق النقر بزر الفأرة الأيمن ثم اختيار Add ثم New Folder): Pages و Entities. الأصناف ضمن المجلّد Entities أضف الأصناف التالي إلى المجلّد Entities: FeedItem و Section و GroupSection. بالنسبة للصنف FeedItem فلقد تحدثنا عنه في الجزء الأوّل، وإليك الشيفرة البرمجيّة الخاصّة به: namespace EnhancedFeedReader.Entities { public class FeedItem { public string Link { get; set; } public string Title { get; set; } public string Description { get; set; } public override string ToString() { return Title; } } } الصنف Section هو صنف جديد تمامًا وهو يمثّل قسم من أقسام المحتوى الخاصّة بموقع أكاديميّة حسّوب. الشيفرة البرمجيّة للملف Section.cs هي: using System; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Xml.Linq; namespace EnhancedFeedReader.Entities { public class Section { public string Name { get; set; } public string ResourceUrl { get; set; } public async Task<ObservableCollection<FeedItem>> GetFeedItems() { return await Task.Factory.StartNew(() => { XDocument doc = XDocument.Load(ResourceUrl); var feeds = from newsItem in doc.Descendants("item") select new FeedItem { Title = newsItem.Element("title").Value, Description = newsItem.Element("description").Value, Link = newsItem.Element("link").Value }; return new ObservableCollection<FeedItem>(feeds); }); } public override string ToString() { return Name; } } } يحتوي هذا الصنف على خاصيّتين: الاسم Name وعنوان المصدر ResourceUrl الذي سيتم جلب الخلاصات منه. يوجد أيضًا التابع GetFeedItems الذي سيجلب الخلاصات التابعة للقسم الحالي. هذا التابع هو البديل للتابع LoadFeeds الذي كان موجودًا في الصنف SectionFeedsPage وهو يقوم بنفس وظيفته مع ميزة استخدام البرمجة غير المتزامنة. وأخيرًا التابع ToString للحصول على تمثيل نصّي لكائن من النوع Section. أمّا بالنسبة للصنف GroupSection فهو يمثّل تصنيف لمجموعة من الأقسام. في الحقيقة يعمل موقع أكاديميّة حسوب على تقسيم الخلاصات ضمن مجموعات كل منها يحتوي على قسمين. انظر إلى الشيفرة الخاصّ بهذا الصنف: using System.Collections.ObjectModel; namespace EnhancedFeedReader.Entities { public class GroupSection : ObservableCollection<Section> { public string Name { get; set; } } } لاحظ كم هي بسيطة هذه الشيفرة. يحتوي هذا الصنف على خاصيّة واحدة هي خاصيّة الاسم Name له. مع الانتباه إلى أنّه يرث من الصنف العمومي ObservableCollection<Section>. وهذا إشارة إلى أنّه يمثّل مجموعة من الأقسام Section. سنرى سبب عمليّة الوراثة هذه بعد قليل. الواجهات ضمن المجلّد Pages يوجد لدينا ضمن هذا المجلّد ثلاث واجهات تتبع لثلاثة أصناف. صنفان منهما قديمان وهما: SectionFeedsPage و FeedDetailsPage وهما يمثّلان على الترتيب: الواجهة المسؤولة عن عرض خلاصات قسم محدّد من الأقسام المتوفّرة، ومحتوى الخلاصة التي اختارها المستخدم من الواجهة السابقة. أمّا الصنف الثالث فهو SectionsPage وهو يمثّل الواجهة الرئيسيّة الخاصة بعرض جميع الأقسام المتوفّرة ضمن موقع الأكاديميّة. انتقل إلى ملف الرماز SectionFeedsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="EnhancedFeedReader.Pages.SectionFeedsPage"> <StackLayout> <ListView x:Name="lsvFeeds" ItemTapped="lsvFeeds_ItemTapped"/> </StackLayout> </ContentPage> ثم انتقل إلى ملف الشيفرة الموافق له وهو SectionFeedsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using EnhancedFeedReader.Entities; using Xamarin.Forms; namespace EnhancedFeedReader.Pages { public partial class SectionFeedsPage : ContentPage { public SectionFeedsPage(Section section) { InitializeComponent(); Title = section.Name; PopulateFeedsListView(section); } private async void PopulateFeedsListView(Section section) { lsvFeeds.ItemsSource = await section.GetFeedItems(); } private async void lsvFeeds_ItemTapped(object sender, ItemTappedEventArgs e) { FeedItem selectedFeed = (FeedItem)e.Item; FeedDetailsPage feedDetailsPage = new FeedDetailsPage(selectedFeed); await Navigation.PushAsync(feedDetailsPage); } } } الجديد في هذا الصنف هو التابع PopulateFeedsListView الذي يُمرّر إليه وسيط واحد من النوع Section حيث يتم الحصول على خلاصاته ومن ثمّ إسنادها إلى القائمة lsvFeeds لعرضها. لاحظ كيف يتمّ ذلك باستخدام البرمجة غير المتزامنة. انتقل بعد ذلك إلى ملف الرماز FeedDetailsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="EnhancedFeedReader.Pages.FeedDetailsPage"> <StackLayout Orientation="Vertical"> <WebView x:Name="wvDescription" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"/> </StackLayout> </ContentPage> ثم انتقل إلى ملف الشيفرة البرمجيّة الموافق له وهو FeedDetailsPage.xaml.cs: using Xamarin.Forms; using EnhancedFeedReader.Entities; namespace EnhancedFeedReader.Pages { public partial class FeedDetailsPage : ContentPage { public FeedDetailsPage(FeedItem feedItem) { InitializeComponent(); this.Title = feedItem.Title; var descriptionHtmlSource = new HtmlWebViewSource(); descriptionHtmlSource.Html = @"<html dir='rtl'><body>" + feedItem.Description + "</body></html>"; wvDescription.Source = descriptionHtmlSource; } } } لم يطرأ في الحقيقة أيّ تعديل على هذا الصنف. انتقل أخيرًا إلى ملف الرماز SectionsPage.xaml الذي يمثّل واجهة العرض الأساسيّة، التي ستعرض جميع أقسام الخلاصات المتاحة في الأكاديميّة واحرص على أن تكون محتوياته كما يلي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="EnhancedFeedReader.Pages.SectionsPage"> <ListView x:Name="lsvSections" ItemTapped="lsvSections_ItemTapped" IsGroupingEnabled="True" GroupDisplayBinding="{Binding Name}" /> </ContentPage> واضح أنّ محتوياته بسيطة، فهي لا تتعدى عنصر القائمة الذي أسميته lsvSections. الأمر الملفت للنظر هنا هو وجود الخاصيتين IsGroupingEnabled و GroupDisplayBinding لعنصر القائمة. أسندنا القيمة True للخاصيّة IsGroupingEnabled وذلك للإشارة إلى وجوب أن تدعم هذه القائمة ميزة التجميع Grouping. أمّا الخاصيّة GroupDisplayBinding فقد أسندت لها القيمة {Binding Name} للإشارة إلى أنّني أرغب بأن يتم ربطها مع قيمة الخاصيّة Name للكائن GroupSection فهي تُحدّد النص المراد إظهاره كعنوان لكل مجموعة. لكي تتوضّح الأمور بشكل أفضل انظر إلى الواجهة في حالة العمل: انتقل الآن إلى ملف الشيفرة البرمجيّة الموافق وهو SectionsPage.xaml.cs لكي نتعرّف على طريقة الاستفادة من هذه الميزة، واحرص على أن تكون محتوياته على الشكل التالي: using System.Collections.Generic; using System.Collections.ObjectModel; using Xamarin.Forms; using EnhancedFeedReader.Entities; namespace EnhancedFeedReader.Pages { public partial class SectionsPage : ContentPage { public SectionsPage() { InitializeComponent(); Title = "قارئ خلاصات أكاديمية حسوب"; LoadGroupSections(); } private void LoadGroupSections() { int spacePos = 0; string tmp; GroupSection gs = null; Dictionary<string, string[]> groupSectionDic = new Dictionary<string, string[]>() { { "خلاصات ريادة الأعمال",new string[] { "https://academy.hsoub.com/entrepreneurship/?rss=1", "https://academy.hsoub.com/questions/rss/entrepreneurship-5.xml" } }, { "خلاصات العمل الحر",new string[] { "https://academy.hsoub.com/freelance/?rss=1", "https://academy.hsoub.com/questions/rss/freelance-8.xml" } }, { "خلاصات التسويق والمبيعات", new string[] { "https://academy.hsoub.com/marketing/?rss=1", "https://academy.hsoub.com/questions/rss/marketing-7.xml" } }, { "خلاصات البرمجة", new string[] { "https://academy.hsoub.com/programming/?rss=1", "https://academy.hsoub.com/questions/rss/programming-3.xml" } }, { "خلاصات التصميم", new string[] { "https://academy.hsoub.com/design/?rss=1", "https://academy.hsoub.com/questions/rss/design-4.xml" } }, { "خلاصات DevOps",new string[] { "https://academy.hsoub.com/devops/?rss=1", "https://academy.hsoub.com/questions/rss/devops-6.xml" } }, { "خلاصات البرامج والتطبيقات",new string[] { "https://academy.hsoub.com/apps/?rss=1", "https://academy.hsoub.com/questions/rss/apps-9.xml" } }, { "خلاصات الشهادات المتخصصة",new string[] { "https://academy.hsoub.com/certificates/?rss=1", "https://academy.hsoub.com/questions/rss/certificates-10.xml" } } }; ObservableCollection<GroupSection> groupSections = new ObservableCollection<GroupSection>(); foreach (var grp in groupSectionDic) { gs = new GroupSection() { Name = grp.Key }; spacePos = grp.Key.IndexOf(" "); tmp = grp.Key.Substring(spacePos + 1); gs.Add( new Section { Name = "مقالات " + tmp , ResourceUrl = grp.Value[0] } ); gs.Add( new Section { Name = "أسئلة " + tmp, ResourceUrl = grp.Value[1] } ); groupSections.Add(gs); } lsvSections.ItemsSource = groupSections; } private async void lsvSections_ItemTapped(object sender, ItemTappedEventArgs e) { Section selectedSection = (Section)e.Item; SectionFeedsPage sectionFeedsPage = new SectionFeedsPage(selectedSection); await Navigation.PushAsync(sectionFeedsPage); } } } هناك الكثير من المتعة في الشيفرة السابقة! تتمحور معظم الشيفرة حول التابع LoadGroupSections ويُستَخدم لتجهيز الأقسام المختلفة لمصادر الخلاصات في الأكاديمية ووضعها ضمن مجموعات منفصلة ضمن عنصر القائمة lsvSections. هذه الأقسام (كعناوين) موجودة في وضع عدم اتصال وقد نسختها يدويًا من موقع الأكاديميّة، وهي ضمن المتغير groupSectionDic الذي صرحنا عنه من نوع القاموس Dictionary ملف التطبيق App.cs انتقل إلى ملف التطبيق App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = new NavigationPage(new SectionsPage()); } احرص على استخدام فضاء الاسم Pages لكي تستطيع الوصول إلى الصفحة SectionsPage كما يلي: using EnhancedFeedReader.Pages; ستضع السطر السابق أوّل الملف App.cs كما هو معلوم. نفّذ البرنامج وتنقّل في أقسامه المختلفة. الخلاصة تناولنا في هذا الدرس الجزء الثاني من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسّوب. لقد أجرينا تحسينات مهمّة على هذا التطبيق من خلال السماح للمستخدم بتصفّح الخلاصات من جميع أقسام الموقع. حيث استخدمنا ميزة التجميع grouping لعنصر القائمة ListView وذلك لتجميع العناوين التابعة لنفس القسم معًا. نكون بذلك قد انتهينا من هذا التطبيق.
- 1 تعليق
-
- xamarin-forms
- c-sharp
-
(و 4 أكثر)
موسوم في:
-
سنتناول في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms تطبيقًا عمليًا آخرًا وهو تطبيق قارئ الخلاصات من موقع أكاديمية حسوب. تندرج مثل هذه التطبيقات تحت النوع rss feed reader ولها مزايا كثيرة ومتنوّعة. سيكون تطبيقنا بسيطًا وعمليًّا ويوضّح مبدأ العمل كما جرت العادة. سننفّذ التطبيق على مرحلتين: المرحلة الأولى سيكون تطبيق لعرض الخلاصات الموجودة ضمن أحد أقسام الموقع، مع إمكانية اختيار أيّ خلاصة وعرض تفاصيل حولها، وهذا هو محتوى هذا الدرس. أمّا المرحلة الثانية فستكون تحسين للتطبيق المنجز في هذا الدرس حيث سنضيف إمكانيّة قراءة الخلاصات من جميع أقسام الموقع مع إضافة بعض التحسينات على أسلوب العرض بشكل عام، وسيكون ذلك في درس منفصل. ماهي الخلاصات؟ خلاصات موقع تكون عادةً عبارة عن مستند XML يحتوي على بيانات تمثّل آخر الأخبار التي يوفّرها الموقع. لا تمتلك جميع المواقع خلاصات بالطبع، ولكنّ من الجيّد دومًا أن يوفّر الموقع مثل هذه الخلاصات في حال كان يمتلك محتوىً متجدّدًا كما هو الحال في موقع أكاديميّة حسوب. بما أنّ الخلاصة تكون ضمن ملف XML فمن البديهي أن تكون البيانات منسّقة بتنسيق XML، أي على شكل عقد آباء وأبناء. انظر مثلًا إلى جزء من خلاصات قسم مقالات البرمجة في أكاديميّة حسوب كما ظهرت لي عند كتابة هذا المقال: لاحظ أنّني أستعرض هذه الخلاصات عن طريق متصفّح Chrome عن طريق الرابط: https://academy.hsoub.com/programming/?rss=1 وهو الرابط الذي توفّره الأكاديميّة للحصول على خلاصات مقالات البرمجة. من الواضح أنّ هذا الأسلوب غير عملي في الحصول على آخر الخلاصات، لذلك يلجأ المستخدمون عادةً إلى تطبيقات متنوّعة لعرض هذه الخلاصات بشكل مريح ومقروء. تطبيق قارئ الخلاصات سنبني في هذا الدرس تطبيق قارئ خلاصات وظيفته قراءة الخلاصات الموجودة في قسم مقالات البرمجة في موقع أكاديميّة حسّوب. وسنعمل في الدرس التالي على تحسين هذا التطبيق بإتاحة الإمكانيّة لتصفّح الخلاصات من جميع الأقسام. أنشئ مشروعًا جديدًا من النوع Blank App (Xamarin.Forms Portable) وسمّه BasicFeedReader ثم أبق فقط على المشروعين BasicFeedReader (Portable) و BasicFeedReader.Droid كما وسبق أن فعلنا في هذا الدرس. الصنف FeedItem من نافذة مستكشف الحل Solution Explorer انقر بزر الفأرة الأيمن على المشروع BasicFeedReader واختر من القائمة التي ستظهر الخيار Add ثم من القائمة الفرعية الخيار New Folder لإضافة مجلّد جديد. سمّ هذا المجلّد بالاسم Entities، وبعد أن يظهر في نافذة الحل Solution Explore انقر عليه بزر الفأرة الأيمن واختر الخيار Add ومن القائمة الفرعية اختر Class. ستظهر نافذة تسمح لك بتعيين اسم لهذا الصنف. اختر الاسم FeedItem له. هذا الصنف هو حجر البناء الأساسي لهذا البرنامج. احرص على جعل محتويات الملف FeedItem.cs كما يلي: namespace BasicFeedReader.Entities { public class FeedItem { public string Link { get; set; } public string Title { get; set; } public string Description { get; set; } public override string ToString() { return Title; } } } الخصائص Link و Title و Description في هذا الصنف تقابل العناصر link و title و description في مستند XML. أمّا التابع ToString فهو للحصول على تمثيل نصّي لأي كائن من النوع FeedItem. الواجهات أضف مجلّدًا جديدًا للمشروع BasicFeedReader كما فعلنا قبل قليل، وسمّه Pages. ثم أضف إليه صفحتي محتوى تعتمدان رماز XAML بحيث يكون اسم الصفحة الأولى هو FeedDetailsPage وهي الواجهة الرئيسيّة، واسم الصفحة الثانية SectionFeedsPage وهي واجهة التفاصيل. الصفحة SectionFeedsPage هي الواجهة التي ستعرض خلاصات قسم مقالات البرمجة في أكاديمّية حسوب. أمّا الواجهة FeedDetailsPage فهي لعرض الخلاصة التي يتم اختيارها من الواجهة SectionFeedsPage. الواجهة SectionFeedsPage انتقل إلى الملف SectionFeedsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="BasicFeedReader.Pages.SectionFeedsPage"> <StackLayout> <ListView x:Name="lsvFeeds" ItemTapped="lsvFeeds_ItemTapped"/> </StackLayout> </ContentPage> لاحظ كم هو بسيط هذا الرماز، فهو لا يحتوي سوى عنصر قائمة lsvFeeds لعرض خلاصات قسم مقالات البرمجة، حيث سنربط الحدث ItemTapped الخاص بها بالمعالج lsvFeeds_ItemTapped. انتقل الآن إلى ملف الشيفرة البرمجيّة الموافق SectionFeedsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using BasicFeedReader.Entities; using System.Linq; using System.Xml.Linq; using Xamarin.Forms; namespace BasicFeedReader.Pages { public partial class SectionFeedsPage : ContentPage { public SectionFeedsPage() { InitializeComponent(); LoadFeeds("https://academy.hsoub.com/programming/?rss=1"); Title = "مقالات البرمجة"; } private async void lsvFeeds_ItemTapped(object sender, ItemTappedEventArgs e) { FeedItem selectedFeed = (FeedItem)e.Item; FeedDetailsPage feedDetailsPage = new FeedDetailsPage(selectedFeed); await Navigation.PushAsync(feedDetailsPage); } private void LoadFeeds(string resource) { XDocument doc = XDocument.Load(resource); var feeds = from newsItem in doc.Descendants("item") select new FeedItem { Title = newsItem.Element("title").Value, Description = newsItem.Element("description").Value, Link = newsItem.Element("link").Value }; lsvFeeds.ItemsSource = feeds; } } } عند يتم إنشاء كائن من هذه الصفحة لعرضها للمستخدم، يتم تنفيذ بانيتها. حيث يعمل التطبيق على استدعاء التابع LoadFeeds والذي يتطلّب وسيطًا واحدًا من النوع string ويمثّل عنوان المصدر المزوّد للخلاصات. في الحقيقة ما فعلناه هنا هو أمر غير جيّد من الناحية العمليّة، فلا ينبغي وضع مثل هذا الاستدعاء هنا في البانية، سيما وأنّ التابع LoadFeeds لا يستخدم تقنية البرمجة غير المتزامنة، مما سيسبب جمودًا مزعجًا في التطبيق عند أوّل تشغيله. سنحل هذه المشكلة لاحقًا في الجزء الثاني. انظر الآن إلى تعريف التابع LoadFeeds من الشيفرة السابقة. ستلاحظ أنّه يستخدم تقنيّة ممتازة يوفرها إطار العمل .NET من خلال الصنف XDocument الذي يمثّل مستند XML بشكل كائني، حيث يعمل السطر الأوّل من هذا التابع على إنشاء كائن جديد من الصنف XDocument من خلال تحميل مستند XML مباشرةً من الانترنت عن طريق التابع الساكن Load. بعد ذلك يتم إعراب مستند XML المحمّل من الإنترنت بسرعة وفعاليّة عاليتين. حيث تحتاج إلى عدد قليل من الأسطر البرمجيّة على شكل استعلام LINQ to XML لكي تقوم بالمطلوب. لقد اطلعنا على تقنيّة شبيهة في درس سابق حيث استخدمنا LINQ to Objects. يمكنك معرفة المزيد حول هذا الموضوع بقراءة هذا المقال. بعد استخلاص المعلومات من مستند XML المُحمّل، وإنشاء كائن من النوع FeedItem لكل خلاصة موجودة في هذا المستند، يتم إسناد المجموعة المنشأة من هذه الكائنات إلى الخاصيّة ItemsSource من القائمة lsvFeeds ليتم عرضها للمستخدم. أمّا بالنسبة لمعالج الحدث lsvFeeds_ItemTapped من الشيفرة السابقة. فوظيفته بسيطة، وهي تنحصر في الحصول على كائن FeedsItem الموجود ضمن العنصر الذي تمّ نقره (لمسه) ضمن القائمة lsvFeeds ومن ثمّ الانتقال إلى الصفحة FeedDetailsPage لعرض محتوى هذه الخلاصة. الواجهة FeedDetailsPage انتقل إلى الملف FeedDetailsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="BasicFeedReader.Pages.FeedDetailsPage"> <StackLayout Orientation="Vertical"> <WebView x:Name="wvDescription" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"/> </StackLayout> </ContentPage> لاحظ مرّة أخرى كم هو بسيط هذا الرماز. حيث يقتصر على استخدام عنصر واحد، هو العنصر WebView ويُستخدم لعرض محتوى مستند HTML. وسبب استخدامي لهذا العنصر، هو أنّ الخلاصات التي ترد من موقع أكاديميّة حسّوب تحتوي على المحتوى كاملًا وهو منسّق بتنسيق HTML. وهذا أمر لا تجده في كثير من مزوّدات خدمة الخلاصات. لذلك فوجدت أنّ أفضل طريقة هو عرض هذا المحتوى مباشرةً ضمن عنصر WebView. انتقل الآن إلى ملف الشيفرة البرمجيّة الموافق FeedDetailsPage.xaml.cs واحرص على أن يكون كما في الشكل التالي: using Xamarin.Forms; using BasicFeedReader.Entities; namespace BasicFeedReader.Pages { public partial class FeedDetailsPage : ContentPage { public FeedDetailsPage(FeedItem feedItem) { InitializeComponent(); this.Title = feedItem.Title; var descriptionHtmlSource = new HtmlWebViewSource(); descriptionHtmlSource.Html = @"<html dir='rtl'><body>" + feedItem.Description + "</body></html>"; wvDescription.Source = descriptionHtmlSource; } } } كل الشيفرة البرمجيّة موجودة ضمن البانية التي تتطلّب وسيطًا واحدًا من النوع FeedItem الذي يحتوي على بيانات الخلاصة المراد عرض تفاصيلها. نعمل على وضع عنوان هذه الصفحة ليكون مطابقًا لعنوان الخلاصة، ثمّ ننشئ كائنًا من النوع HtmlWebViewSource نسنده ضمن المتغيّر descriptionHtmlSource الذي سيمثّل الكائن المحتوى للعنصر wvDescription (عنصر WebView الذي صرّحنا ضمن ملف الرماز). لاحظ كيف أسندنا للخاصيّة Html لهذا المتغيّر محتوى الخاصيّة Description لكائن الخلاصة، وهو كما أشرنا قبل قليل عبارة عن مستند HTML ينقصه فقط الوسمين و اللذان أضفناهما يدويًّا. ثمّ نُسند المتغيّر descriptionHtmlSource بدوره إلى الخاصيّة Source للعنصر wvDescription مما يؤدّي إلى ظهور مستند HTML كما هو مخطّط له. ملف التطبيق App.cs انتقل إلى ملف التطبيق App.cs واحرص على أن تكون بانية الصنف App على الشكل التالي: public App() { // The root page of your application MainPage = MainPage = new NavigationPage(new SectionFeedsPage()); } احرص على استخدام فضاء الاسم Pages لكي تستطيع الوصول إلى الصفحة SectionFeedsPage كما يلي: using BasicFeedReader.Pages; ستضع السطر السابق أوّل الملف App.cs كما هو معلوم. نفّذ البرنامج، ستحصل على شكل شبيه بما يلي: وهي الواجهة التي تحتوي على الخلاصات الموجودة ضمن قسم مقالات البرمجة. جرّب اختيار أحد هذه الخلاصات لتحصل على شكل شبيه بما يلي: لاحظ كيف تظهر الخلاصة كما لو أنّه يتم عرضها ضمن متصفّح الويب العادي. جرّب سحب الصفحة إلى الأسفل ليؤكّد ذلك هذه الملاحظة. الخلاصة تناولنا في هذا الدرس الجزء الأوّل من تطبيق قارئ الخلاصات الخاص بموقع أكاديميّة حسّوب. حيث نفّذنا بعض المهام الأساسيّة، والتي سنبني عليها في الجزء الثاني الذي سيتناول تطبيقًا محسّنًا لهذا التطبيق. حيث سنعمل على دعم عرض جميع الأقسام المتاحة في الأكاديميّة وليس قسم مقالات البرمجة فحسب.
-
هذا الدرس هو الجزء الثالث والأخير من سلسلة دروس تُعنى بكيفيّة بناء تطبيق عملي بسيط لإدارة جهات اتصال ببيانات أوليّة وهو بطبيعة الحال جزء من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms. ستحتاج إلى قراءة الجزأين الأوّل والثاني السابقين لكي تستطيع المتابعة في هذا الدرس. سنهتم في هذا الدرس ببناء واجهتي التطبيق: الواجهة الرئيسيّة التي يمكن من خلالها البحث عن جهات الاتصال حسب الاسم والكنية، وأيضًا إضافة جهة اتصال جديدة. الواجهة الخاصة بعرض تفاصيل جهة الاتصال التي تمّ اختيارها من الواجهة الرئيسيّة، ومن ثمّ إمكانيّة تعديل بياناتها أو حتى حذفها. بالإضافة إلى تعديل الصنف App لكي يصبح التطبيق قابل للعمل. الواجهة الرئيسيّة للتطبيق ContactsPage سنحتاج في البداية إلى إضافة مجلّد جديد سنسمّه Pages سنضع فيه أي صفحة جديدة للتطبيق، وهذا الإجراء هو من باب تنظيم مكوّنات التطبيق فحسب. من نافذة مستكشف الحل Solution Explorer انقر بزر الفأرة الأيمن على اسم المشروع ContactsApp (Portal) ثم اختر من القائمة التي ستظهر الخيار Add. من القائمة الفرعية، اختر New Folder ثمّ سمّه Pages. انقر بزر الفأرة الأيمن على المجلّد الذي أضفته توًا وهو Pages واختر من القائمة Add ثم اختر من القائمة الفرعية New Item لتظهر نافذة تسمح باختيار العنصر الجديد الذي تودّ إضافته. اختر صفحة محتوى تعتمد على الرماز كما وأن سبق لنا أن فعلنا ذلك في هذا الدرس. سمّ هذه الصفحة ContactsPage. انتقل الآن إلى الملف ContactsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ContactsApp.Pages.ContactsPage" Appearing ="ContactsPage_Appearing"> <StackLayout> <StackLayout Padding="5,25,5,5"> <StackLayout Orientation="Horizontal"> <Label Text="First Name" /> <Editor x:Name="txtFirstName" HorizontalOptions="FillAndExpand" TextChanged="Editor_TextChanged"/> </StackLayout> <StackLayout Orientation="Horizontal"> <Label Text="Last Name" /> <Editor x:Name="txtLastName" HorizontalOptions="FillAndExpand" TextChanged="Editor_TextChanged"/> </StackLayout> <Button Text="Find" x:Name="btnFind" Clicked="btnFind_Clicked"/> </StackLayout> <StackLayout> <ListView x:Name="lsvContacts" ItemTapped="lsvContacts_ItemTapped"/> </StackLayout> <Button Text="+ New Contact" HorizontalOptions="FillAndExpand" Clicked="btnNewContact_Clicked"/> </StackLayout> </ContentPage> لقد استخدمنا أربعة أحداث مختلفة في هذه الصفحة سنورد وصفًا قصيرًا لكل منها فيما يلي: حدث الظهور Appearing للصفحة ContactsPage وقد ربطناه بالمعالج ContactsPage_Appearing وذلك لتحديث بيانات الصفحة عند العودة من الصفحة المسؤولة عن عرض تفاصيل جهة الاتصال كما سنرى لاحقًا في هذا الدرس. حدث تغيّر النص TextChangedلكلّ من العنصرين txtFirstName و txtLastName وقد ربطناه بالمعالج Editor_TextChanged وذلك لمسح نتائج البحث التي تظهر ضمن القائمة lsvContacts عند أي تعديل يجريه المستخدم فيهما. حدث لمس المُدخل ضمن القائمة lsvContacts وربطناه بالمعالج lsvContacts_ItemTapped وذلك لكي ينتقل البرنامج إلى الواجهة المسؤولة عن عرض تفاصيل جهة الاتصال عند لمسها. حدث النقر Clicked لزر إضافة جهة اتصال جديدة وربطناه بالمعالج btnNewContact_Clicked. انتقل بعد ذلك إلى ملف الشيفرة البرمجيّة الموافق للملف السابق وهو ContactsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using System; using Xamarin.Forms; using ContactsApp.Entities; namespace ContactsApp.Pages { public partial class ContactsPage : ContentPage { public ContactsPage() { InitializeComponent(); } private async void DoFind() { string firstName = txtFirstName.Text == null ? "" : txtFirstName.Text; string lastName = txtLastName.Text == null ? "" : txtLastName.Text; lsvContacts.ItemsSource = await((App)Application.Current).ContactsRepository .GetContactsAsync(firstName, lastName); } private void btnFind_Clicked(object sender, EventArgs e) { DoFind(); } private void Editor_TextChanged(object sender, TextChangedEventArgs e) { if (lsvContacts.ItemsSource != null) lsvContacts.ItemsSource = null; } private async void lsvContacts_ItemTapped(object sender, ItemTappedEventArgs e) { Contact selectedContact = (Contact)e.Item; ContactDetailsPage ContactDetailsPage = new ContactDetailsPage(selectedContact); await Navigation.PushAsync(ContactDetailsPage); } private void ContactsPage_Appearing(object sender, EventArgs e) { DoFind(); } private async void btnNewContact_Clicked(object sender, EventArgs e) { ContactDetailsPage ContactDetailsPage = new ContactDetailsPage(null); await Navigation.PushAsync(ContactDetailsPage); } } } يحتوي هذا الملف على الصنف ContactsPage والذي يحتوي على معالجات الأحداث التي صرّحنا عنها في صفحة الرماز الموافقة له كما رأينا قبل قليل، بالإضافة إلى وجود تابع خدمي وهو DoFind ووظيفته إجراء عمليّة بحث وفقًا للمعايير التي يرغبها المستخدم (الاسم والكنية). الشيفرة البرمجيّة الموجودة ضمن معالجات الأحداث بسيطة وواضحة. ولكن أريد أن أتوقّف قليلًا عند الشيفرة البرمجيّة التي يستخدمها التابع الخدمي DoFind التي تنفّذ عمليّة البحث: string firstName = txtFirstName.Text == null ? "" : txtFirstName.Text; string lastName = txtLastName.Text == null ? "" : txtLastName.Text; lsvContacts.ItemsSource = await((App)Application.Current).ContactsRepository .GetContactsAsync(firstName, lastName); أوّل سطرين واضحان حيث يعملان على الحصول على معايير البحث التي يريدها المستخدم من مربّعي النص txtFirstName و txtLastName. أمّا السطر الأخير فهو يعمل على الاتصال بالمستودع لتنفيذ عمليّة البحث وإرجاع النتائج وذلك بإجراء استدعاء إلى التابع GetContactsAsync وتمرير معياري البحث إليه. انظر هذه العبارة: await((App)Application.Current).ContactsRepository .GetContactsAsync(firstName, lastName); تعمل الخاصية Application.Current على إرجاع كائن من النوع Application يمثّل التطبيق الحالي الذي نعمل من خلاله. والذي يحتاج بدوره إلى عملية تحويل cast باستخدام التحويل (App) إلى كائن من النوع App وهو الصنف الأساسي في التطبيق. في الحقيقة لقد صرّحت عن الخاصيّة ContactsRepository في الصنف App ليتم الوصول للمستودع من أيّ مكان من تطبيقنا. سنرى التصريح عن هذه الخاصيّة بعد قليل. واجهة تفاصيل جهة الاتصال ContactDetailsPage أضف هذه الواجهة بنفس الأسلوب الذي اتبعناه عند إضافة ملف الواجهة الرئيسيّة، أي إلى المجلّد Pages. على أن يكون اسمها ContactDetailsPage. سيصبح مستكشف الحل لديك شبيه بما يلي: انتقل إلى ملف الرماز ContactDetailsPage.xaml واحرص على أن تكون محتوياته على الشكل التالي: <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="ContactsApp.Pages.ContactDetailsPage"> <StackLayout Orientation="Vertical" Padding="5,25,5,5"> <Label Text="FirstName" /> <Entry x:Name ="txtFirstName"/> <Label Text="LastName" /> <Entry x:Name ="txtLastName"/> <Label Text="Tel" /> <Entry x:Name ="txtTel"/> <Label Text="EMail" /> <Entry x:Name ="txtEMail"/> <Label Text="Hobbies" /> <Editor x:Name ="txtHobbies"/> <StackLayout Orientation="Horizontal"> <Button Text="Save" x:Name="btnSave" Clicked="btnSave_Clicked"/> <Button Text="Delete" x:Name="btnDelete" Clicked="btnDelete_Clicked"/> </StackLayout> </StackLayout> </ContentPage> واضح أنّه ملف بسيط. هناك حدثان قد صرّحنا عنهما في هذه الصفحة وهما: حدث النقر لزر الحفظ Clicked وقد ربطناه بالمعالج btnSave_Clicked وهو مسؤول عن عمليتي الحفظ والإضافة. حدث النقر لزر الحذف Clicked وقد ربطناه بالمعالج btnDelete_Clicked وهو مسؤول عن حذف جهة الاتصال. انتقل إلى ملف الشيفرة البرمجيّة الموافق لملف الرماز السابق واسمه ContactDetailsPage.xaml.cs واحرص على أن تكون محتوياته على الشكل التالي: using System; using Xamarin.Forms; using ContactsApp.Entities; namespace ContactsApp.Pages { public partial class ContactDetailsPage : ContentPage { private Contact currentContact; public ContactDetailsPage(Contact contact) { InitializeComponent(); this.currentContact = contact; if (this.currentContact != null) { txtFirstName.Text = contact.FirstName; txtLastName.Text = contact.LastName; txtEMail.Text = contact.EMail; txtTel.Text = contact.Tel; txtHobbies.Text = contact.Hobbies; Title = contact.ToString() + " Details"; btnDelete.IsVisible = true; } else { Title = "New Contact"; btnDelete.IsVisible = false; } } private async void btnSave_Clicked(object sender, EventArgs e) { string firstName = txtFirstName.Text == null ? "" : txtFirstName.Text; string lastName = txtLastName.Text == null ? "" : txtLastName.Text; string tel = txtTel.Text == null ? "" : txtTel.Text; string email = txtEMail.Text == null ? "" : txtEMail.Text; string hobbies = txtHobbies.Text == null ? "" : txtHobbies.Text; if (this.currentContact != null) { this.currentContact.FirstName = firstName; this.currentContact.LastName = lastName; this.currentContact.EMail = tel; this.currentContact.Tel = email; this.currentContact.Hobbies = hobbies; await ((App)Application.Current).ContactsRepository .UpdateContactAsync(this.currentContact); } else { Contact contact = new Contact { FirstName = firstName, LastName = lastName, Tel = tel, EMail = email, Hobbies = hobbies, }; await ((App)Application.Current).ContactsRepository .AddContactAsync(contact); } await Navigation.PopAsync(); } private async void btnDelete_Clicked(object sender, EventArgs e) { var result = await DisplayAlert("Delete Confirmation", "Are you sure you want to delete this contact?", "Yes", "No"); if (result) { await ((App)Application.Current).ContactsRepository .DeleteContactAsync(this.currentContact); await Navigation.PopAsync(); } } } } كما وأوضحنا مسبقًا أنّ وظيفة هذه الواجهة هي عرض تفاصيل جهة اتصال وتعديلها، مع إمكانيّة حذفها، بالإضافة إلى استخدام هذه الواجهة في إضافة جهة اتصال جديدة أيضًا. تميّز هذه الواجهة الغرض المطلوب منها عن طريق كائن من النوع Contact يُمرّر كوسيط إلى بانيتها. إذا كان هذا الوسيط يحتوي على كائن صالح من النوع Contact فهذا يعني أنّنا نريد عرض جهة الاتصال التي يمثّلها هذا الكائن، ومن ثمّ تعديلها أو حذفها، ويقتضي ذلك بالطبع إظهار زر الحذف Delete. أمّا في حال تمّ تمرير null كوسيط إلى البانية، فهذا يعني أنّه تمّ استدعاء الصفحة لإنشاء جهة اتصال جديدة تمامًا، وبالتالي إخفاء زر الحذف Delete لأنّه لن يكون له معنى في هذه الحالة. يتم حفظ نسخة من الكائن الممرّر ضمن حقل خاص ضمن الصنف اسمه currentContact. ويتم تحديد الغاية من الواجهة ضمن البانية نفسها. انظر إلى تعريف معالج الحدث btnSave_Clicked من الشيفرة السابقة وانظر كيف يتعامل مع الحالتين السابقتين (تعديل أو إضافة). الصنف App انتقل إلى الملف App.cs واحرص على أن تكون محتوياته على الشكل التالي: using Xamarin.Forms; using ContactsApp.Abstract; using ContactsApp.Concrete; using ContactsApp.Pages; namespace ContactsApp { public class App : Application { public IContactsRepository ContactsRepository { get; set; } public App() { ContactsRepository = new MemoryContactsRepository(); // The root page of your application MainPage = new NavigationPage(new ContactsPage()); } protected override void OnStart() { // Handle when your app starts } protected override void OnSleep() { // Handle when your app sleeps } protected override void OnResume() { // Handle when your app resumes } } } لاحظ بدايةّ الخاصيّة ContactsRepository من النوع IContactsRepository الموجود ضمن تعريف الصنف App: public IContactsRepository ContactsRepository { get; set; } في الحقيقة ستمثّل هذه الخاصيّة المستودع الذي سنتبادل البيانات من خلاله. سنعمل على إنشاء كائن جديد من النوع MemoryContactsRepository ومن ثمّ نسنده إلى هذه الخاصيّة وذلك ضمن بانية الصنف App كما يلي: ContactsRepository = new MemoryContactsRepository(); وهذا الأمر جائز تمامًا لأنّ الصنف MemoryContactsRepositroy يحقّق الواجهة IContactsRepository فهو بمثابة وارث منها. بما أنّ هذه الخاصيّة عامّة public فسيكون بإمكان جميع أجزاء التطبيق الوصول للكائن ContactsRepository وبالتالي التعامل مع البيانات من خلال مكان واحد. لاحظ أيضًا من بانية الصنف App كيف أنشأنا كائن جديد من الصنف NavigationPage وأسندناه إلى الخاصيّة MainPage للصنف App. وذلك لأنّنا نريد أن يدعم تطبيقنا ميزة التنقّل بين الصفحات التي تحدثنا عنها في هذا الدرس. يمكنك الآن تنفيذ التطبيق وتجربة جميع المزايا التي يتمتّع بها. الخلاصة نكون بهذا الدرس قد أنهينا بناء تطبيق جهات الاتصال، حيث تحدثنا عن كيفيّة بناء واجهتي التطبيق، وكيفيّة التنقّل بين هاتين الواجهتين، وكيف تتصّلان بالمستودع عند الحاجة للتعامل مع مصدر البيانات الذي يكون في تطبيقنا هذا عبارة عن مجموعة تتكوّن من عناصر من النوع Contact موجودة في ذاكرة التطبيق.
-
- 1
-
- xamarin-forms
- c-sharp
-
(و 3 أكثر)
موسوم في:
-
سنبدأ في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms ببناء تطبيق عملي بمعايير تقنيّة عالية. حيث سنستخدم المعارف التي حصلنا عليها من الدروس السابقة في بناء تطبيق جهات اتصال بسيط لكنّه يستخدم تقنيّات ومفاهيم متقدّمة نسبيًّا. سنتناول هذا التطبيق على ثلاثة أجزاء متتالية، إليك وصف مختصر لمحتوى كلّ منها: الجزء الأوّل: شرح الغاية من التطبيق، وتوضيح فكرة نموذج المستودع Repository في بناء التطبيقات، مع بناء الهيكل العام للتطبيق، وهذا هو محتوى هذا الدرس. الجزء الثاني: تجهيز النواحي الوظيفيّة للمستودع وجعله قابلًا للاستخدام. الجزء الثالث: تنفيذ واجهتي التطبيق الرئيسية والفرعيّة الخاصّة بعرض التفاصيل. وتنفيذ عمليّة التنقّل بين الواجهتين الغاية من التطبيق وكيف يعمل فكرة التطبيق بسيطة للغاية، تتلخّص بعرض جهات اتصال موجودة مسبقًا وإمكانية البحث ضمنها، مع إمكانيّة إضافة جهات اتصال جديدة وتحريرها وحذفها. يعتمد التطبيق على وجود واجهتين. الواجهة الأولى هي الواجهة الرئيسيّة وتحتوي على قسم خاص بالبحث حسب الاسم أو الكنية عن أيّ جهة اتصال موجودة مسبقًا، بالإضافة إلى قائمة لعرض جهات الاتصال الناتجة عن عمليّة البحث، وأخيرًا زر خاص بإضافة جهات اتصال جديدة. انظر الشكل التالي الذي ينتج عند ضغط زر البحث FIND عند عدم تحديد أي معيار للبحث: عندما يقوم المستخدم بنقر زر البحث FIND دون أن يحدّد أي معيار، سيقوم التطبيق بعرض جميع جهات الاتصال الموجودة لديه، والتي ستكون في هذه النسخة من البرنامج عبارة عن بيانات وهمية موجودة ضمن ذاكرة التطبيق. أمّا عند تحديد المستخدم للاسم أو الكنيّة فسيعمل التطبيق على البحث مستخدمًا منطق AND. أمّا الواجهة الثانية، فتظهر عندما يلمس المستخدم إحدى جهات الاتصال من القائمة السابقة، حيث تعرض هذه الواجهة بيانات تفصيليّة حول جهة الاتصال هذه: الاسم والكنية ورقم الهاتف وعنوان البريد الإلكتروني والهوايات. انظر إلى الشكل التالي: من الممكن تعديل أيّ من هذه البيانات ثم ينقر المستخدم زر الحفظ لحفظها، أو أن ينقر زر الرجوع إلى الواجهة السابقة الموجود في الأعلى بجانب أيقونة البرنامج في حال لم يرغب بتعديل البيانات. كما يمكن للمستخدم أن يحذف جهة الاتصال هذه بنقره على زر الحذف Delete كما يظهر من الشكل السابق. وهذه ببساطة فكرة التطبيق. نموذج المستودع Repository عندما تكبر التطبيقات وتتنوّع المهام المطلوبة منها تبرز الحاجة لوسيلة لتنظيم العمل داخل التطبيق. في الحقيقة توجد العديد من النماذج التي تدعمها Xamarin لهذه الغاية مثل نموذج MVVM الذي يستخدم بفعالية ضمن Xamarin لتنظيم وفصل الأجزاء المسؤولة عن الواجهات عن الأجزاء المسؤولة عن منطق العمل عن تلك المسؤولة عن التعامل مع مزودات البيانات البعيدة أو المحلية باختلاف أنواعها. من النماذج التي أفضلها شخصيًّا هو نموذج المستودع Repository الذي أستخدمه على نحو واسع في جميع أنواع التطبيقات التي أعمل عليها. فهو أسلوب جميل ومنطقي ويسمح بتطوير التطبيق بشكل سلس وسريع للعمل في مختلف أنواع البيئات، وهو متوافق للعمل مع نموذج MVVM. يسمح نموذج المستودع بعزل الشيفرة البرمجيّة المسؤولة عن التعامل مع البيانات عن منطق البرنامج business logic. وفي هذا الأمر عدة فوائد من أهمّها: تنظيم البرنامج، وجعله أكثر قابليّة للفهم والتطوير. إجراء تطوير على أسلوب التعامل مع البيانات دون إجراء أي تغيير في منطق عمل البرنامج. إمكانيّة إجراء تغيير جذري لنوع الخدمة التي نستخدمها لتخزين البيانات دون تغيير يُذكر في منطق العمل. سأخوض مباشرةً في كيفية اعتماد هذا النموذج في تطبيقنا هذا. حيث سنحتاج إلى استخدام واجهة واحدة Interface مع صنف واحد يُحقّقها. لتنعش ذاكرتك حول الواجهات انظر هذا الدرس. لنبدأ الآن في بناء هذا التطبيق وذلك في الفقرة التالية. بناء التطبيق ابدأ بإنشاء مشروع جديد من النوع Blank App (Xamarin.Forms Portable) وسمّه ContactsApp ثم أبق فقط على المشروعين ContactsApp (Portable) و ContactsApp.Droid كما وسبق أن فعلنا في هذا الدرس. من نافذة مستكشف الحل Solution Explorer انقر بزر الفأرة الأيمن على المشروع ContactsApp واختر من القائمة التي ستظهر الخيار Add ثم من القائمة الفرعية الخيار New Folder لإضافة مجلّد جديد. سمّ هذا المجلّد بالاسم Entities، وبعد أن يظهر في نافذة الحل Solution Explore انقر عليه بزر الفأرة الأيمن واختر الخيار Add ومن القائمة الفرعية اختر Class. ستظهر نافذة تسمح لك بتعيين اسم لهذا الصنف. اختر الاسم Contact له. هذا الصنف هو حجر البناء الأساسي لهذا البرنامج والذي يمثّل منطق العمل فيه. احرص على جعل محتويات الملف Contact.cs كما يلي: namespace ContactsApp.Entities { public class Contact { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Tel { get; set; } public string EMail { get; set; } public string Hobbies { get; set; } public override string ToString() { return string.Concat(FirstName, " ", LastName); } } } يحتوي الصنف Contact كما يظهر من الشكل السابق على البيانات الأساسيّة التي تحتاجها أيّة جهة اتصال، بالإضافة إلى خاصيّة الهوايات Hobbies التي قد تبدو غريبة قليلًا بالنسبة لجهة اتصال. انقر مرّة أخرى بزر الفأرة الأيمن على المشروع ContactsApp ثم اختر من القائمة التي ستظهر الخيار Add ثم من القائمة الفرعية الخيار New Folder لإضافة مجلّد جديد. سمّ هذا المجلّد بالاسم Abstract، وبعد أن يظهر في نافذة الحل Solution Explore انقر عليه بزر الفأرة الأيمن واختر الخيار Add ومن القائمة الفرعية اختر New Item. ستظهر نافذة تسمح لك بتعيين نوع العنصر المراد إضافته. اختر واجهة Interface وعيّن الاسم IContactsRepository لها. واحرص على أن تكون محتويات الملف IContactsRepository.cs كما يلي: using System.Threading.Tasks; using System.Collections.ObjectModel; using ContactsApp.Entities; namespace ContactsApp.Abstract { public interface IContactsRepository { Task<ObservableCollection<Contact>> GetContactsAsync(string firstName, string lastName); Task<bool> AddContactAsync(Contact contactToAdd); Task<bool> UpdateContactAsync(Contact contactToUpdate); Task<bool> DeleteContactAsync(Contact contactToDelete); } } تُستَخدَم الواجهات عمومًا عندما نرغب بتجريد Abstraction الأمور وجعلها عموميّةً وفي ذلك فائدة كبيرة في جعل الشيفرة البرمجيّة أكثر قابليّة للفهم ولإعادة الاستخدام. وهذا سبب إضافة هذه الواجهة إلى المجلّد Abstract. لا تحتوي الواجهات على أيّة شيفرة برمجيّة كما نعلم، فكل ما تحتويه هو عبارة عن تصاريح لتوابع يجب تحقيقها ضمن أيّ صنف يرغب بتحقيق هذه الواجهة. تحتوي هذه الواجهة باختصار على العمليّات الأساسيّة التي يحتاجها تطبيقنا لإنجاز المهام المنوطة به وهي: الحصول على جهات الاتصال حسب الاسم والكنية GetContactsAsync، وإضافة جهة اتصال جديدة AddContactAsync، وتحديث جهة اتصال موجودة مسبقًا UpdateContactAsync، وحذف جهة اتصال DeleteContactAsync. أمّا سبب وجود الكلمة Async في كلّ من هذه التوابع فهو للإشارة إلى أنّه يُفترض بها أن تستخدم تقنيّة البرمجة غير المتزامنة Asynchronous Programming التي تحدثنا عنها في هذا الدرس. المثير في الأمر أنّ هذه الواجهة لا تهتم بمكان وجود البيانات أو كيفيّة الحصول عليها والتعامل معها. إنّما تهتم فقط بما يحتاجه التطبيق وبشكل مجرّد. سنكرّر الآن نفس العمليّة لإضافة مجلّد جديد ضمن المشروع ContactsApp.cs واسمه Concrete وهو الذي سيحتوي على الصنف الذي سيحقّق الواجهة IContactsRepository السابقة. انقر بزر الفأرة الأيمن على هذا المجلّد واختر Add. ومن القائمة الفرعية اختر Class. سمّ هذا الصنف بالاسم MemoryContactsRepository واحرص على أن تكون محتوياته كما يلي: using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Collections.ObjectModel; using ContactsApp.Abstract; using ContactsApp.Entities; namespace ContactsApp.Concrete { public class MemoryContactsRepository : IContactsRepository { private ObservableCollection<Contact> contacts; public MemoryContactsRepository() { contacts = new ObservableCollection<Contact>() { new Contact() { Id=1, FirstName = "Ahmad", LastName="Saeed", Tel="123456", EMail="admin@example.com", Hobbies="Swimming" }, new Contact() { Id=2, FirstName = "Mahmood", LastName="Maktabi", Tel="852136", EMail="info@example.com", Hobbies="Reading" }, new Contact() { Id=3, FirstName = "Mazen", LastName="Najem", Tel="987456", EMail="it@example.com", Hobbies="Swimming" }, new Contact() { Id=4, FirstName = "Sawsan", LastName="Hilal", Tel="741258", EMail="sales@example.com", Hobbies="Writing, Reading" }, new Contact() { Id=5, FirstName = "Musab", LastName="Aga", Tel="357159", EMail="admin@example.com", Hobbies="Sport" } }; } public async Task<ObservableCollection<Contact>> GetContactsAsync(string firstName, string lastName) { throw new System.NotImplementedException(); } public async Task<bool> AddContactAsync(Contact contactToAdd) { throw new System.NotImplementedException(); } public async Task<bool> UpdateContactAsync(Contact contactToUpdate) { throw new System.NotImplementedException(); } public async Task<bool> DeleteContactAsync(Contact contactToDelete) { throw new System.NotImplementedException(); } } } الأمر الملفت للنظر هنا أنّ بيانات جهات الاتصال موجودة ضمن هذا الصنف بالفعل وتحديدًا ضمن بانيته. وهي مخزّنة ضمن المتغيّر contacts وهو معرّف على مستوى الصنف ومن النوع العمومي ObservableCollection الذي سنتحدّث عنه في الدرس التالي. يكفي الآن أن تعرف أنّه عبارة عن مجموعة عناصرها كائنات من النوع Contact وهي تُفيد في التطبيقات التي تُستخدَم فيها البرمجة غير المتزامنة. من غير الواقعي بكل تأكيد وجود البيانات مخزّنة في الصنف بهذه الطريقة، فالمفترض أن تكون ضمن قاعدة بيانات محليّة أو بعيدة أو حتى ضمن ملف عادي. في الواقع تبرز هنا قوّة نموذج المستودع Repository في عزل أسلوب الحصول على البيانات عن البرنامج الفعلي. من الواضح أيضًا أنّ التوابع الموجودة هنا تحتوي في الحقيقة على شيفرة برمجيّة تؤدّي إلى رمي الاستثناء NotImplementedException وذلك لتذكرينا بعدم جاهزيتها بعد. سيبدو مستكشف الحل في نهاية المطاف شبيهًا بالشكل التالي: الخلاصة هذا الدرس هو المقدّمة لتطبيق جهات الاتصال الذي سنتابع بناءه على مدى الدرسين التاليين. تناولنا في هذا الدرس فكرة التطبيق الأساسيّة، وتوضيح فكرة نموذج المستودع Repository من خلال بناء واجهة تمثّله بالإضافة إلى صنف يحقّقها. سيمكننا من خلال هذا الصنف (صنف المستودع) التعامل مع بيانات موجودة ضمن ذاكرة التطبيق فقط. ورغم كون هذا الأسلوب غير واقعي، إلَّا أنّه ضروري في تبسيط الأمور وجعلها أسهل للفهم. كما أنشأنا صنف يمثّل جهة الاتصال في التطبيق. ورأينا ماهية العلاقة بينه وبين صنف المستودع، من خلال الاستدعاءات إلى التوابع الموجودة ضمن الصنف الأخير. حقوق الصورة البارزة محفوظة لـ Freepik
-
سنتابع في هذا الدرس من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام Xamarin.Forms العمل الذي بدأناه في الدرس السابق والمتمثّل ببناء تطبيق جهات الاتصال. قد أنهينا في الدرس السابق بناء نموذج المستودع من خلال التصريح عن الواجهة IContactsRepository وتحقيقها من خلال الصنف MemoryContactsRepository. كما أنشأنا الصنف Contacts الذي يمثّل حجر البناء الأساسي في التطبيق. سنضيف في هذا الدرس النواحي الوظيفيّة للصنف المستودع MemoryContactsRepository لكي يصبح تطبيقنا قابلًا للعمل. تجهيز النواحي الوظيفيّة للمستودع افتح الملف MemoryContactsRepository.cs واحرص على أن تكون محتوياته على الشكل التالي: using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using System.Collections.ObjectModel; using ContactsApp.Abstract; using ContactsApp.Entities; namespace ContactsApp.Concrete { public class MemoryContactsRepository : IContactsRepository { private ObservableCollection<Contact> contacts; public MemoryContactsRepository() { contacts = new ObservableCollection<Contact>() { new Contact() { Id=1, FirstName = "Ahmad", LastName="Saeed", Tel="123456", EMail="admin@example.com", Hobbies="Swimming" }, new Contact() { Id=2, FirstName = "Mahmood", LastName="Maktabi", Tel="852136", EMail="info@example.com", Hobbies="Reading" }, new Contact() { Id=3, FirstName = "Mazen", LastName="Najem", Tel="987456", EMail="it@example.com", Hobbies="Swimming" }, new Contact() { Id=4, FirstName = "Sawsan", LastName="Hilal", Tel="741258", EMail="sales@example.com", Hobbies="Writing, Reading" }, new Contact() { Id=5, FirstName = "Musab", LastName="Aga", Tel="357159", EMail="admin@example.com", Hobbies="Sport" } }; } public async Task<ObservableCollection<Contact>> GetContactsAsync(string firstName, string lastName) { return await Task.Factory.StartNew(() => { IEnumerable<Contact> result = from contact in contacts where contact.FirstName .ToUpper() .Contains(firstName.ToUpper()) && contact.LastName .ToUpper() .Contains(lastName.ToUpper()) select contact; ObservableCollection<Contact> tmp = new ObservableCollection<Contact>(result); return tmp; }); } public async Task<bool> AddContactAsync(Contact contactToAdd) { return await Task.Factory.StartNew(() => { contactToAdd.Id = contacts.Count() + 1; contacts.Add(contactToAdd); return true; }); } public async Task<bool> UpdateContactAsync(Contact contactToUpdate) { return await Task.Factory.StartNew(() => { Contact result = (from contact in contacts where contact.Id == contactToUpdate.Id select contact).FirstOrDefault(); if (result != null) { contactToUpdate.FirstName = result.FirstName; contactToUpdate.LastName = result.LastName; contactToUpdate.EMail = result.EMail; contactToUpdate.Tel = result.Tel; contactToUpdate.Hobbies = result.Hobbies; return true; } else { return false; } }); } public async Task<bool> DeleteContactAsync(Contact contactToDelete) { return await Task.Factory.StartNew(() => { var result = (from contact in contacts where contact.Id != contactToDelete.Id select contact); if (result != null) { contacts = new ObservableCollection<Contact>(result); contactToDelete = null; return true; } else { return false; } }); } } } الجديد هنا أنّنا قد أسندنا شيفرة برمجيّة لكل من التوابع الأساسيّة الموجودة في المستودع. انظر إلى الفقرات التالية التي تشرح عمل الشيفرة البرمجيّة ضمن كل تابع. تابع البحث GetContactsAsync فيما يلي التابع GetContactsAsync والذي يتطلّب وسيطين من النوع string للبحث حسب الاسم والكنية لجهات الاتصال: public async Task<ObservableCollection<Contact>> GetContactsAsync(string firstName, string lastName) { return await Task.Factory.StartNew(() => { IEnumerable<Contact> result = from contact in contacts where contact.FirstName .ToUpper() .Contains(firstName.ToUpper()) && contact.LastName .ToUpper() .Contains(lastName.ToUpper()) select contact; ObservableCollection<Contact> tmp = new ObservableCollection<Contact>(result); return tmp; }); } واضح أنّ هذه الشيفرة تقوم بإنشاء مهمة جديدة (تابع على شكل تعبير lambda) عن طريق التابع Task.Factory.StartNew (ستحتاج إلى إنعاش ذاكرتك بهذا الدرس) وذلك باستخدام تقنية البرمجة غير المتزامنة لمنع جمود التطبيق كما نعلم. المتغيّر result الذي تراه في الشيفرة السابق هو من النوع IEnumerable<Contact> أي مجموعة قابلة للعد عناصرها من النوع Contact، وهو يحصل على هذه المجموعة من خلال استعلام LINQ To Objects بسيط: from contact in contacts where contact.FirstName .ToUpper() .Contains(firstName.ToUpper()) && contact.LastName .ToUpper() .Contains(lastName.ToUpper()) select contact; تعمل هذه الشيفرة ببساطة على الاستعلام عن جميع جهات الاتصال الموجودة ضمن المتغير contacts والتي تحتوي على عبارتي البحث الخاصّتين بالاسم والكنية بنفس الوقت. إذا كان لديك خبرة باستعلامات SQL فستجد LINQ to Objects مألوفة. لاحظ وجود عبارتي return ضمن هذا التابع GetContactsAsync. العبارة التي تأتي أولًا هي العبارة المسؤولة عن إرجاع كائن من النوع Task<ObservableCollection<Contact>> أمّا العبارة الثانيّة التي تأتي في الأسفل فهي المسؤولة عن إرجاع كائن من النوع Task<ObservableCollection<Contact>> من التابع الداخلي وهو عبارة عن تعبير lambda والذي يتم تنفيذه من خلال التابع Task.Factory.StartNew كما هو واضح. في آخر سطرين من التابع الداخلي (تعبير lambda) نعمل على تقديم نتيجة البحث على شكل مجموعة عموميّة وهي ObservableCollection<Contact> حيث يتم تحويلها ضمنيًّا إلى كائن من النوع Task<ObservableCollection<Contact>>. تابع إضافة جهة اتصال جديد AddContactAsync التابع AddContactAsync يتطلّب وسيطًا واحدًا فقط من النوع Contact ويُرجع كائن من النوع Task<bool> للإشارة إلى نجاح عمليّة الإضافة: public async Task<bool> AddContactAsync(Contact contactToAdd) { return await Task.Factory.StartNew(() => { contactToAdd.Id = contacts.Count() + 1; contacts.Add(contactToAdd); return true; }); } تعمل هذه الشيفرة على إضافة جهة اتصال جديدة إلى جهات الاتصال الموجودة مسبقًا ضمن المتغيّر contacts مع الانتباه إلى هذه العبارة البرمجيّة: contactToAdd.Id = contacts.Count() + 1; ما تفعله هذه العبارة غير موجود في أيّ تطبيق عمليّ حقيقي. فهي تُسند قيمة للخاصيّة Id لجهة الاتصال الممرّرة إلى التابع. لا ينبغي لأي تطبيق أن يقوم بهذا العمل، فهذا الرقم ينبغي أن يتم توليده تلقائيًّا عند إضافة جهة الاتصال إلى قاعدة البيانات. وبما أنّ تطبيقنا يعتمد على بيانات وهمية موجودة في الذاكرة فكان لزامًا علينا القيام بمثل هذا الأمر لتمييز جهات الاتصال المضافة حديثًا. تابع الحذف DeleteContactAsync التابع DeleteContactAsync يتطلّب وسيطًا واحدًا فقط من النوع Contact ويُرجع كائن من النوع Task<bool> للإشارة إلى نجاح عمليّة الحذف: public async Task<bool> DeleteContactAsync(Contact contactToDelete) { return await Task.Factory.StartNew(() => { var result = (from contact in contacts where contact.Id != contactToDelete.Id select contact); if (result != null) { contacts = new ObservableCollection<Contact>(result); contactToDelete = null; return true; } else { return false; } }); } تعمل الشيفرة السابقة في الواقع على فلترة جهة الاتصال المراد حذفها مما يعني ببساطة أنّ استعلام LINQ to Objects سيعمل على إرجاع جميع جهات الاتصال باستثناء تلك التي يكون قيمة الخاصية Id لها مطابقة لقيمة الخاصيّة Id لجهة الاتصال التي نرغب بحذفها. ومن ثمّ يتم إسناد النتيجة إلى المتغيّر contacts مما يعني فعليًّا أنّنا قد حذفنا جهة الاتصال هذه من الذاكرة. تابع التحديث UpdateContactAsync التابع UpdateContactAsync والذي يتطلّب وسيطًا واحدًا فقط من النوع Contact ويُرجع كائن من النوع Task<bool> للإشارة إلى نجاح عمليّة الحذف: public async Task<bool> UpdateContactAsync(Contact contactToUpdate) { return await Task.Factory.StartNew(() => { Contact result = (from contact in contacts where contact.Id == contactToUpdate.Id select contact).FirstOrDefault(); if (result != null) { contactToUpdate.FirstName = result.FirstName; contactToUpdate.LastName = result.LastName; contactToUpdate.EMail = result.EMail; contactToUpdate.Tel = result.Tel; contactToUpdate.Hobbies = result.Hobbies; return true; } else { return false; } }); } تخضع هذه الشيفرة البرمجيّة لنفس المبدأ: يبحث استعلام LINQ to Objects عن جهة الاتصال مطلوبة ضمن جهات الاتصال الموجودة ضمن المتغيّر contacts حسب قيمة الخاصيّة Id لجهة الاتصال، وبعد الحصول عليها، يتم تحديث قيم الخصائص الأخرى كما هو واضح. بهذه الطريقة أصبح نموذج المستودع جاهزًا لوضعه في الاستخدام. حيث ستتعامل معه جميع مكوّنات التطبيق لإنجاز أربعة أنواع مختلفة من العمليّات على البيانات: البحث، والإضافة، والتعديل، والحذف. الخلاصة تناولنا في هذا الدرس كيفيّة بناء نموذج المستودع الذي يمثّله الصنف MemoryContactsRepository والذي يحقّق كما نعلم الواجهة IContactsRepository حيث تعرّفنا على التوابع الأربعة ضمنه التي تسمح لمكوّنات التطبيق بالتعامل مع البيانات دون الاهتمام بمكان وجود هذه البيانات. اطلعنا أيضًا على كيفيّة توظيف تقنيّة الاستعلام LINQ to Objects ضمن التوابع الأربعة لتنفيذ المهام المطلوبة من هذا المستودع. سنبدأ في الدرس التالي ببناء واجهات التطبيق. حقوق الصورة البارزة محفوظة لـ Freepik
-
- xamarin-forms
- c-sharp
-
(و 3 أكثر)
موسوم في: