المزامنة مع outlook

مرحبا تحدث في المقالة السابقة كيف يمكنك التواصل مع Outlook من خلال استخدام مكتبة Micosoft.Office, بعد الانتهاء من العمل على المشروع الذي احتجت به التواصل مع الـ Outlook كان كل شيء يعمل طبيعيا من داخل visual studio حيث يمكنني تشغيل تطبيق asp.net و إضافة و تعديل و حذف مواعيد من داخل asp.net , و لكن عندما أخذت التطبيق إلى production و نشرته إلى IIS واجهة بعض المتاعب كون انه لا يمكنك التواصل مع تطبيق desktop من داخل IIS .

عندها كان علي أن اجد طريقة للمزامنة مع outlook و طبعا يظهر الحل القديم و معهود عند المحاولة لتبادل البيانات بين التطبيق و هي تخزين السطر في جدول معين في قاعدة البيانات مع الاشارة إلى ان هذا السطر بحاجة إلى نشر , ثم يقوم تطبيق اخر مثلا windows Service يعمل كل فترة معينة بالبحث عن السطور  التي تحتاج نشر إلى الـ Outlook في هذه الحالة, ثم يقوم بنشرها مع تعديل خصائصها بأنه تم نشرها.
image

الحل جيد إلى حد كبير و لكنه غير عملي لأسباب كثيرة أولا المزامنة ليست حقيقة بمعنى هناك فارق زمني بين الضغط على أمر نشر في تطبيق asp.net و بين timer control التي تقوم بالبحث كل 10 ثواني مثلا عن بيانات جديدة, المشكلة الثانية هي استهلاك كم كبير من الموارد بشكل غير متوقع فكل عشر ثواني عليك أن تتفتح اتصال بقاعدة بيانات و تنفيذ جملة استعلام للتحقق من أن هناك مواعيد جديدة لم تنشر ثم إذا وجدت مواعد جديدة عليك نشرها و تعديل حالة نشرها في قاعدة البيانات.

هناك حل أخر لا يعتمد على خادم قاعدة البيانات و لا على IIS و إنما على المستخدم نفسه, بمعنى أخر ما اريد عمله الأن هو عندما يقوم المستخدم بضغط على نشر سيقوم تطبيق asp.net بإخبار الـ Console Application في حالتي بأن هناك موعد جديدة عليك نشره راجع قاعدة البيانات , انظر الصورة
image

رائع صحيح , بهذه الطريقة سأتخلص تمام من فكرة المؤقت المعاقة :-) , سيكون هناك مزامنة حقيقة بتو و اللحظة للبيانات سأخفض بشكل كبير استهلاك موارد الاتصال و قاعدة البيانات و غيرها, و هنا يأتي دور SingalR , شاهد الفيديو:



الفكرة بسيطة جدا سأقوم أولا بإنشاء hub جديد اسميته NotificationHub حيث يتولى هذا الـ Hub مهمة التواصل مع الـ Client يحتوي الـ Hub على طريقة واحدة هي notifyClientsToStartSync و التي تستعدي الطريقة All الموجدة داخل الكائن Client , لاحظ هنا بان NotifyClients عبارة عن كائن Dynimc , الشفرة :
   1: // <copyright file="Error.aspx.cs" company="GCC">
   2: //     GCC. All rights reserved.
   3: // </copyright>
   4: // <author>ALGHABBAN</author>
   5: // <email>a.alghabban@gcc-sg.org</email>
   6:  
   7: namespace Agenda.Web
   8: {
   9:     using Microsoft.AspNet.SignalR;
  10:  
  11:     /// <summary>
  12:     /// Notification Hub
  13:     /// </summary>
  14:     public class NotificationHub : Hub
  15:     {
  16:         /// <summary>
  17:         /// Gets or sets withere Client Need To Sync or not 
  18:         /// </summary>
  19:         public static bool ClientNeedToSync { get; set; }
  20:  
  21:         /// <summary>
  22:         /// notify ClientsTo Start Sync
  23:         /// </summary>
  24:         /// <param name="clientDoneFormSynicinig">client Done Form Synicinig</param>
  25:         public void notifyClientsToStartSync(bool clientDoneFormSynicinig)
  26:         {
  27:             if(ClientNeedToSync)
  28:             {                
  29:                 Clients.All.NotifyClients();
  30:                 ClientNeedToSync = clientDoneFormSynicinig;
  31:             }            
  32:         }
  33:     }
  34: }

يجب علي أن اقوم بنقطة أخرى هو self-hosting للـ SignalR , حيث يجب على التطبيق asp.net ان يقوم باستضافة تطبيق SignalR بداخلة و بتالي أصبح هو كخادم و أن يعمل بمجرد عمل التطبيق, لعمل ذلك عليا استخدام الحدث Application_Start و استدعاء الطريقة WebApp.Start ممرا لها مكان أو رابط الاستضافة , الشفرة :

   1: /// <summary>
   2: /// Start the SignalR Server when the application start
   3: /// </summary>
   4: /// <param name="sender">sender object</param>
   5: /// <param name="e">e object</param>
   6: protected void Application_Start(object sender, EventArgs e)
   7: {
   8:  string ServerURI = System.Configuration.ConfigurationManager.AppSettings["SignalRLink"].ToString();
   9:  
  10:     try
  11:     {
  12:         SignalR = WebApp.Start(ServerURI);
  13:     }
  14:     catch (Exception ex)
  15:     {
  16:         new Logger(ex);
  17:     }                        
  18: }

نأتي لشفرة الاهم و هي اخبار العميل بأن هناك تحديثات جديدة طرأت و لعمل ذلك سنقوم بإنشاء طريقة اسميتها NotifiyClients موجودة داخل التصنيف Global و هي Static , الشفرة :

   1: /// <summary>
   2: /// Notifi All Client change happend
   3: /// </summary>
   4: public static void NotifiyClients()
   5: {
   6:     var context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();
   7:     context.Clients.All.notifyClientsToStartSync(true);
   8: }

لأن كل ما علي فعله هو استدعاء هذه الطريقة في الاماكن التي اريدها كما شاهدة في الفيديو عند الضغط على زر نشر أو الغاء نشر في او تعديل او حذف موعد , الشفرة , على سبيل المثال في حالة الحذف :
   1: /// <summary>
   2: /// delete selected program
   3: /// </summary>
   4: /// <param name="selectedProgram">selected Program to delete</param>
   5: private void DeleteProgram(Agenda.Controller.Program selectedProgram)
   6: {
   7:     try
   8:     {
   9:         if (selectedProgram != null)
  10:         {
  11:             selectedProgram.AgentStatus = "Deleted";
  12:             selectedProgram.Update();                    
  13:             this.ProgramGridView.DataBind();
  14:             Global.NotifiyClients();
  15:         }
  16:     }
  17:     catch (Exception ex)
  18:     {
  19:         new Logger(ex);
  20:         this.Response.Redirect("~/Error.aspx");
  21:     }
  22: }

نأتي للجزء الاخر من القصة وهو الـ Client , أقوم أولا عند بداية تشغيل التطبيق بإنشاء تواجد من للـ SignalR و من ثم اقوم بعمل Proxy مستخدما الاتصال بـ، HubConnection أخيرا استخدام الطريقة On لتنفيذ الطريقة notifyClientsToStartSync التي قمت بإنشائها مسبقا ممرا لها SyncToOutLook, الشفرة :

   1: /// <summary>
   2: /// Wait To Syinc with Agenda Data Base
   3: /// </summary>
   4: /// <returns></returns>
   5: static async Task WaitToSyncFromAgendDataBase()
   6: {
   7:     try
   8:     {
   9:         Connection = new HubConnection(ServerURI);
  10:         HubProxy = Connection.CreateHubProxy("NotificationHub");
  11:         HubProxy.On<bool>("notifyClientsToStartSync", (NotificationStatus) =>
  12:         {
  13:             if (NotificationStatus)
  14:             {
  15:                 SyncToOutlook();
  16:             }
  17:         });
  18:  
  19:         await Connection.Start();
  20:         Console.WriteLine("Connted to the server");
  21:     }
  22:     catch (Exception ex)
  23:     {
  24:         Console.Write(ex.Message);
  25:     }
  26: }
ليس مهم ما هي الشفرة المكتوبة من اجل الطريقة SyncToOutLook ما هو مهم الان انني استعديت هذه الطريقة عنده الحاجة لها فقط, موفرة بذلك الكثير من الوقت و الجهد و الموارد و مزامنة حقيقة بين التطبيق و بين outlook.

العمل مع Outlook

كثيرا ما نواجه متطلبات من المستخدمين لربط التطبيقات مع outlook  أقوم حاليا بالعمل على مشروع يقوم بمزامنة مواعد المستخدمين مع outlook من اجل الاستفادة من الخدمات التي يقدمها Microsoft outlook خصوصا عند الحديث عن مزامنة مع الهواتف الذكية, سأتحدث في المقالة عن إنشاء و حذف و البحث و تعديل موعد في Outlook للمستخدم الحالي.

بعيدا عن المقدمات لنبدأ بالعمل , في الحقيقة لا توجد طرق مباشرة لتعامل مع Outlook فعليك العمل مع الـ COM الخاص بها, و طبعا كونك ستتعامل مع COM فستفقد العديد من المميزات, للبدء العمل أضف مكتبة GeniusesCode.Outlook تحتوي المكتبة على التصنيف Appointment و الذي يمثل موعد و التصنيف Calender و الذي يمثل التقويم الخاص بالمستخدم.
   1: // <copyright file="Appointment.cs" company="Geniuses Code">
   2: // Copyright (c) 2014 All Rights Reserved
   3: // </copyright>
   4:  
   5: namespace GeniusesCode.Outlook
   6: {
   7:     using System;
   8:  
   9:     /// <summary>
  10:     /// Appointment Object
  11:     /// //summary>
  12:     public class Appointment
  13:     {
  14:         /// <summary>
  15:         /// Gets or sets Appointment ID
  16:         /// </summary>
  17:         public int ID { get; set; }
  18:  
  19:         /// <summary>
  20:         /// Gets or sets whiter Appointment us All Day Event
  21:         /// </summary>
  22:         public bool AllDayEvent { get; set; }
  23:  
  24:         /// <summary>
  25:         /// Gets or sets Appointment Body
  26:         /// </summary>
  27:         public string Body { get; set; }
  28:  
  29:         /// <summary>
  30:         /// Gets or sets Appointment Category
  31:         /// </summary>
  32:         public string Category { get; set; }
  33:  
  34:         /// <summary>
  35:         /// Gets or sets Appointment Compny
  36:         /// </summary>
  37:         public string Copmany { get; set; }
  38:  
  39:         /// <summary>
  40:         /// Gets or sets Appointment  Creation Time
  41:         /// </summary>
  42:         public DateTime CreationTime { get; set; }
  43:  
  44:         /// <summary>
  45:         /// Gets or sets when the Appointment End At
  46:         /// </summary>
  47:         public DateTime End { get; set; }
  48:  
  49:         /// <summary>
  50:         /// Gets or sets Appointment Location
  51:         /// </summary>
  52:         public string Location { get; set; }
  53:  
  54:         /// <summary>
  55:         /// Gets or sets Appointment Reminder Minutes Before Start
  56:         /// </summary>
  57:         public int ReminderMinutesBeforeStart { get; set; }
  58:  
  59:         /// <summary>
  60:         /// Gets or sets Appointment 
  61:         /// </summary>
  62:         public bool ReminderPlaySound { get; set; }
  63:  
  64:         /// <summary>
  65:         /// Gets or sets when the Appointment will start
  66:         /// </summary>
  67:         public DateTime Start { get; set; }
  68: 
  69:         /// <summary>
  70:         /// Gets or sets the subject of the Appointment
  71:         /// </summary>
  72:         public string Subject { get; set; }
  73:     }
  74: }


يمثل هذا التصنيف كائن موعد بدلا عن الكائن الخاص بـ Outlook , حيث سنتعامل معه لتخزين بيانات الموعد . الخطوة التالية هي إنشاء موعد جديد و لعمل ذلك سأقوم بإنشاء جديد اسميته Calender , لعمل موعد جديد سنقوم بإنشاء طريقة جديدة في هذا التصنيف تستخدم الكائن الـ Application  الموجود في Microsoft.Office.Interop.Outlook سيتواصل هذا الكائن مع برنامج outlook الذي يعمل من أجل إجراء اية تعديلات على المواعيد, الشفرة :


   1: /// <summary>
   2: /// Create new Appointment
   3: /// </summary>
   4: /// <param name="appointment">Appointment you want to create</param>
   5: /// <returns>true if Appointment get created</returns>
   6: public bool Create(Appointment appointment)
   7: {
   8:     Microsoft.Office.Interop.Outlook.Application outlookApp = new Microsoft.Office.Interop.Outlook.Application(); // creates new outlook app
   9:     Microsoft.Office.Interop.Outlook.AppointmentItem oAppointment =
  10:         (Microsoft.Office.Interop.Outlook.AppointmentItem)outlookApp.CreateItem(Microsoft.Office.Interop.Outlook.OlItemType.olAppointmentItem);
  11:  
  12:     oAppointment.Subject = appointment.ID + ": " + appointment.Subject; // set the subject
  13:     oAppointment.Body = appointment.Body; // set the body
  14:     oAppointment.Location = appointment.Location; // set the location          
  15:     oAppointment.Start = appointment.Start; // Set the start date 
  16:     oAppointment.End = appointment.End; // End date 
  17:     oAppointment.ReminderSet = true; // Set the reminder
  18:     oAppointment.ReminderMinutesBeforeStart = appointment.ReminderMinutesBeforeStart; // reminder time
  19:     oAppointment.Companies = appointment.Copmany;
  20:     oAppointment.Categories = appointment.Category;
  21:  
  22:     oAppointment.Save();
  23:  
  24:    return true;
  25: }

في هذه الطريقة اقوم باستقبال كائن من النوع Appointment ثم أقوم بإنشاء Application من outlook و اقوم بإنشاء كان من النوع olAppointmentItem ثم اقوم بعمل Mapping بين الكائن olAppintment و الكائن Appointment الخاص بـ GeniusesCode.Outlook , بعد عمل الـ Mapping  اقوم باستدعاء الطريقة Save من أجل حفظ الموعد.

نأتي الان لخطوة التالية و هي الحصول على جميع المواعيد الموجودة في الـ Calender للعمل لذلك قمت بإنشاء الطريقة Get  و التي تعبد List من الـ Appointment , الشفرة :

   1: /// <summary>
   2: /// Get all Appointent
   3: /// </summary>
   4: /// <returns>a list of Appointment</returns
   5: public List<Appointment> Get()
   6: {
   7:     List<Appointment> appointments = null;
   8:     OutLook.MAPIFolder calendarFolder = null;
   9:     OutLook.Application outlookObject = new OutLook.Application();
  10:     calendarFolder = (OutLook.MAPIFolder)outlookObject.Session.GetDefaultFolder(OutLook.OlDefaultFolders.olFolderCalendar);
  11:     if (calendarFolder != null)
  12:     {
  13:         appointments = new List<Appointment>();
  14:         foreach (Microsoft.Office.Interop.Outlook.AppointmentItem appointmentItem in calendarFolder.Items)
  15:         {
  16:             {
  17:                 Appointment appointment = new Appointment();                      
  18:                 appointment.AllDayEvent = appointmentItem.AllDayEvent;
  19:                 appointment.Body = appointmentItem.Body;
  20:                 appointment.Category = appointmentItem.Categories;
  21:                 appointment.Copmany = appointmentItem.Companies;
  22:                 appointment.CreationTime = appointmentItem.CreationTime;
  23:                 appointment.End = appointmentItem.End;
  24:                 appointment.Location = appointmentItem.Location;
  25:                 appointment.ReminderMinutesBeforeStart = appointmentItem.ReminderMinutesBeforeStart;
  26:                 appointment.ReminderPlaySound = appointmentItem.ReminderPlaySound;
  27:                 appointment.Start = appointmentItem.Start;
  28:                 appointment.Subject = appointmentItem.Subject;                    
  29:                 appointments.Add(appointment);
  30:             }
  31:         }
  32:     }
  33:  
  34:     return appointments;
  35: }

الشفرة في الأعلى تقوم بتحميل محتويات المجلد الافتراضي الخاص بـ Calender للمستخدم, بعد الحصول على المجلد اقوم بمرور على كل موعد موجود ضمن هذا المجلد من ثم عمل Mapping لكائن Appointment و الذي أخيرا اقوم بإضافته إلى القائمة التي تعيدها الطريقة.

نحتاج أيضا لطريقة Find , سأبحث في هذه الطريقة عن كائن Appointment من خلال الـ ID حيث تقوم الطريقة Create بإضافة الـ ID الخاص بالموعد قبل الموعد في الحقل Subjet , الشفرة :

   1: /// <summary>
   2: /// Find Appointment By ID
   3: /// </summary>
   4: /// <param name="appointmentID">ID of Appointment</param>
   5: /// <returns>Appointment object</returns>
   6: public Appointment Find(int appointmentID)
   7: {
   8:     List<Appointment> appointments = this.Get();
   9:     if (appointments != null)
  10:     {
  11:         Appointment appointment = appointments.Find(x => x.Subject.StartsWith(appointmentID.ToString() + ": "));
  12:         return appointment;
  13:     }
  14: 
  15:     return null;
  16: }

ما يحدث في الطريقة بسيط للغاية اقوم اولا بتحميل جميع المواعيد داخل List من Appointment ثم استخدم LINQ من أجل البحث عن اي موعد يبدأ العنوان الخاص به برقم الموعد المرر للطريقة Find.

نأتي اخيرا لطريقة Delete , حيث تطلب هذه الطريق رقم الموعد و تقوم بحذفه من جدول المواعيد , الشفرة :

   1: /// <summary>
   2:  /// Delete appointment
   3:  /// </summary>
   4:  /// <param name="appointmentID">appointment id you want to delete</param>
   5:  /// <returns>true if appointment get deleted</returns>
   6:  public bool Delete(int appointmentID)
   7:  {
   8:      OutLook.MAPIFolder calendarFolder = null;
   9:      OutLook.Application outlookObject = new OutLook.Application();
  10:      calendarFolder = (OutLook.MAPIFolder)outlookObject.Session.GetDefaultFolder(OutLook.OlDefaultFolders.olFolderCalendar);
  11:      if (calendarFolder != null)
  12:      {                
  13:          foreach (Microsoft.Office.Interop.Outlook.AppointmentItem appointmentItem in calendarFolder.Items)
  14:          {
  15:              if(appointmentItem.Subject.StartsWith(appointmentID.ToString() + ": "))
  16:              {
  17:                  appointmentItem.Delete();
  18:              }
  19:          }
  20:      }
  21:  
  22:      return true;
  23:  }

للأسف لا يمكنك حذف موعد بشكل مباشر , كوسيلة لتحايل على هذا الامر تقوم الطريقة Delete بتحميل جميع المواعيد و من ثم المرور داخل كل موعد و البحث عن مواعد يبدأ عنوانه برقم المرر للطريقة Delete  أخيرا و عند العثور على الموعد استخدا الطريق Delete الموجودة داخل الكائن AppointmentItem لحذفه.

أخيرا يبقى لدينا الطريق Update مجددا لا توجد طريقة مباشرة لتعديل للنظر لشفرة اولا:

   1: /// <summary>
   2: /// Update appointment
   3: /// </summary>
   4: /// <param name="appointment">appointment you want to update</param>
   5: /// <returns>true if appointment get updated</returns>
   6: public bool Update(Appointment appointment)
   7: {
   8:     this.Delete(appointment.ID);
   9:     this.Create(appointment);
  10:     return true;
  11: }

تقوم الشفرة أولا بحذف المواعد القديم مستخدمة في ذلك الطريقة Delete , و بعد أن تقوم بحذف الموعد تقوم بإعادة كتابته مجددا في جدول المواعد, بهذا اكون انتهيت.