هذا الدرس هو الجزء الثالث والأخير من سلسلة دروس تُعنى بكيفيّة بناء تطبيق عملي بسيط لإدارة جهات اتصال ببيانات أوليّة وهو بطبيعة الحال جزء من سلسلة تعلّم برمجة تطبيقات أندرويد باستخدام 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. وذلك لأنّنا نريد أن يدعم تطبيقنا ميزة التنقّل بين الصفحات التي تحدثنا عنها في هذا الدرس.
يمكنك الآن تنفيذ التطبيق وتجربة جميع المزايا التي يتمتّع بها.
Quoteستبدو القوة التي يتمتّع بها نموذج المستودع جليّة عندما نرغب بتغيير مكان حفظ البيانات، حيث سنحتاج فقط إلى تغيير صنف المستودع (وليس الواجهة) مع بقاء باقي مكوّنات التطبيق دون أي تغيير. بمعنى أدق، سيكون التغيير في مكان واحد فقط وهو ضمن بانية الصنف App، حيث سنغيّر النوع الذي سننشئ منه الكائن الذي نُسنده إلى الخاصيّة ContactsRepository.
الخلاصة
نكون بهذا الدرس قد أنهينا بناء تطبيق جهات الاتصال، حيث تحدثنا عن كيفيّة بناء واجهتي التطبيق، وكيفيّة التنقّل بين هاتين الواجهتين، وكيف تتصّلان بالمستودع عند الحاجة للتعامل مع مصدر البيانات الذي يكون في تطبيقنا هذا عبارة عن مجموعة تتكوّن من عناصر من النوع Contact موجودة في ذاكرة التطبيق.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.