Разработка
11
Июн
6

Уведомление о Действиях

В Outlook’е есть такая очень полезная функция как уведомления о приближении времени начала встречи. Если у Вас настроена интеграция Outlook’а и CRM, то Вы можете ей воспользоваться, чтобы не пропустить созданные в CRM Встречи. Но зачастую данные системы используются раздельно. А приходить на Встречи вовремя все равно хочется 🙂
Я видел разные способы реализации уведомлении о подобных событиях. И сейчас попробуем запилить наиболее сбалансированный (имхо) с точки зрения трудозатрат, бреда и кавайности 🙂

Решение будет состоять из Windows-приложения постоянно висящего в Трее Windows. Данное приложение будет периодически запрашивать в CRM приближающиеся запланированные Действия и уведомлять о них пользователя посредством подсказок. Помимо этого в контекстное меню приложения будут выводиться все найденные Действия и кликнув по которым будет открываться соответствующая карточка CRM.

Приступим:

  • Создайте в VS новое Windows Form приложение с именем crmTrayApp (т.к. у меня CRM 2015, то я использую .Net 4.5.2);
  • Удалите из предложения файл Form.cs (нам он не понадобится);
  • Добавьте в решение в качестве ресурсов следующие иконки:
  • Замените в файле Program.cs код на следующий:
    using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Windows;
    using Microsoft.Xrm.Sdk;
    using Microsoft.Xrm.Sdk.Messages;
    using Microsoft.Xrm.Sdk.Query;
    using Microsoft.Xrm.Sdk.Client;
    using System.ServiceModel;
    using System.ServiceModel.Description;
    using System.Runtime.Serialization;
    using System.Reflection;
    using System.Timers;
    
    namespace crmTrayApp
    {
        public class crmSysTrayApp : Form
        {
            [STAThread]
            public static void Main()
            {
                Application.Run(new crmSysTrayApp());
            }
    
            private NotifyIcon trayIcon;
            private ContextMenuStrip trayMenu;
            private ToolStripMenuItem item;
            IOrganizationService service;
    
            public crmSysTrayApp()
            {
                // Подключаемся к CRM
                ClientCredentials credentials = new ClientCredentials();
                credentials.Windows.ClientCredential = System.Net.CredentialCache.DefaultNetworkCredentials;
                Uri uri = new Uri("http://crm2015/superfirma/XRMServices/2011/Organization.svc");
                OrganizationServiceProxy proxy = new OrganizationServiceProxy(uri, null, credentials, null);
                proxy.ServiceConfiguration.CurrentServiceEndpoint.Behaviors.Add(new ProxyTypesBehavior());
                service = (IOrganizationService)proxy;
    
                // Создаем иконку в Трее
                trayIcon = new NotifyIcon();
                trayIcon.Text = "CRM Remainder";
                trayIcon.Icon = crmTrayApp.Properties.Resources.crmLogo;
                trayIcon.MouseClick += new MouseEventHandler(IconClick); // При клике принудительно пересчитываем Действия
                trayIcon.BalloonTipTitle = "Ближайшие Действия";
                trayIcon.BalloonTipIcon = ToolTipIcon.Info;
                trayIcon.Visible = true;
                // Создаем контекстное меню и добавляем контекстное меню для иконки в Трее
                trayMenu = new ContextMenuStrip();
                trayIcon.ContextMenuStrip = trayMenu;
    
                // Первый раз подсчитываем количество Действий
                PeriodicCheckActivity();
                // ... и запускаем таймер переодического подсчета
                System.Timers.Timer myTimer = new System.Timers.Timer();
                myTimer.Elapsed += delegate { PeriodicCheckActivity(); };
                myTimer.Interval = 600000; // 10 минут
                myTimer.Enabled = true;
                myTimer.Start();
            }
    
            private void IconClick(object sender, MouseEventArgs e)
            {
                // Сбрасываем содержимое контекстного меню
                trayMenu.Items.Clear();
    
                EntityCollection result;
    
                // Возвращаем Действия текущего пользователя на следующий час
                string fetch = @"
                   <fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='true'>
                      <entity name='activitypointer'>
                        <attribute name='subject' />
                        <attribute name='activityid' />
                        <attribute name='scheduledend' />
                        <attribute name='activitytypecode' />
                        <attribute name='instancetypecode' />
                        <attribute name='community' />
                        <order attribute='scheduledend' descending='false' />
                        <filter type='and'>
                          <condition attribute='statecode' operator='in'>
                            <value>0</value>
                            <value>3</value>
                          </condition>
                          <condition attribute='isregularactivity' operator='eq' value='1' />
                          <condition attribute='scheduledend' operator='next-x-hours' value='1' />
                        </filter>
                        <link-entity name='activityparty' from='activityid' to='activityid' alias='ah'>
                          <filter type='and'>
                            <condition attribute='partyid' operator='eq-userid' />
                          </filter>
                        </link-entity>
                      </entity>
                    </fetch>";
    
                result = service.RetrieveMultiple(new FetchExpression(fetch));
    
                // Если Действия найдены
                if (result.Entities.Count > 0)
                {
                    // ... добавляем их в контекстное меню
                    foreach (var a in result.Entities)
                    {
                        DateTime scheduledendLT = (Convert.ToDateTime(a.Attributes["scheduledend"])).ToLocalTime(); // Конвертируем в локальное время
                        String time = string.Format("{0:HH:mm}", scheduledendLT); // Получаем время в формате 24 ч.
                        item = new ToolStripMenuItem(time + " - " + a.Attributes["subject"].ToString());
                        item.Image = (Image)crmTrayApp.Properties.Resources.ResourceManager.GetObject(a.Attributes["activitytypecode"].ToString());
                        item.Image.Tag = a.Attributes["activitytypecode"].ToString(); // Записываем Тип действия
                        item.Tag = a.Attributes["activityid"].ToString(); // Записываем GIUD записи
                        item.Click += new EventHandler(ProcessMenuItem); // При клике на нем вызываем обработчик
                        trayMenu.Items.Add(item);
                    }
                }
                else
                {
                    // ... иначе добавляем пункт меню Ничего не найдено
                    item = new ToolStripMenuItem("Ничего не найдено...");
                    item.Enabled = false; // Отключяем пункт меню
                    trayMenu.Items.Add(item);
                }
    
                // Добавляем пункт меню Выход и разделить в конеткстное меню
                item = new ToolStripMenuItem("Выход");
                item.Click += OnExit; // При клике на нем закрываем приложение
                trayMenu.Items.Add(new ToolStripSeparator());
                trayMenu.Items.Add(item);
    
                // Если клик произведен левой кнопкой мыши - все равно показываем контекстное меню
                if (e.Button == MouseButtons.Left)
                {
                    MethodInfo mi = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
                    mi.Invoke(trayIcon, null);
                }
            }
    
            private void PeriodicCheckActivity()
            {
                // Пересчитываем Действия и получаем их количество
                EntityCollection result;
                string fetch = @"
                    <fetch version='1.0' distinct='true'>
                      <entity name='activitypointer' >
                        <attribute name='activityid'/>
                        <filter type='and' >
                          <condition attribute='statecode' operator='in' >
                            <value>0</value>
                            <value>3</value>
                          </condition>
                          <condition attribute='isregularactivity' operator='eq' value='1' />
                          <condition attribute='scheduledend' operator='next-x-hours' value='1' />
                        </filter>
                        <link-entity name='activityparty' from='activityid' to='activityid'>
                          <filter type='and' >
                            <condition attribute='partyid' operator='eq-userid' />
                          </filter>
                        </link-entity>
                      </entity>
                    </fetch>";
                
                result = service.RetrieveMultiple(new FetchExpression(fetch));
    
                // Если Действия найдены, то показываем подсказку
                if (result.Entities.Count > 0)
                {
                    trayIcon.BalloonTipText = "Найдено Действий: " + result.Entities.Count.ToString();
                    trayIcon.ShowBalloonTip(1000);
                }
            }
    
            private void ProcessMenuItem(object sender, EventArgs e)
            {
                // Формируем и открываем ссылку
                item = (ToolStripMenuItem)sender;
                var Link = String.Format("{0}/{1}/main.aspx?etn={2}&pagetype=entityrecord&id=%7B{3}%7D", "http://crm2015", "superfirma", item.Image.Tag, item.Tag);
                System.Diagnostics.Process.Start(Link);
            }
    
            private void OnExit(object sender, EventArgs e)
            {
                Application.Exit();
            }
    
            protected override void OnLoad(EventArgs e)
            {
                Visible = false; // Скрываем форму приложения
                ShowInTaskbar = false; // Убираем приложение из Панели задач
                base.OnLoad(e);
            }
    
            protected override void Dispose(bool isDisposing)
            {
                if (isDisposing)
                {
                    // Release the icon resource.
                    trayIcon.Dispose();
                }
    
                base.Dispose(isDisposing);
            }
        }
    }
    

    Разбор полетов:

    • Объявляем глобальные переменные;
    • Подключаемся к CRM под текущим пользователем;
    • Создаем иконку в Трее. При этом:
      • Изображение для иконки берем из подключенные ресурсов;
      • При клике на иконке в Трее формируем и показываем контекстное меню из Действий CRM;
      • Подключаем к иконке пустое контекстное меню.
    • При загрузке приложения вызываем первичный запрос приближающихся Действий в CRM, а затем запускаем таймер периодического запроса в CRM текущих Действий;
    • Несколько основных функций:
      • Функция IconClick обрабатывает клик на иконке: заполняет контекстное меню ближайшими Действиями;
      • Функция PeriodicCheckActivity периодически запрашивает в CRM ближайшие Действия и показывает в подсказке их количество;
      • Функция ProcessMenuItem обрабатывает клик на пункте меню — открывает браузер с карточкой Действия. GUID и объект записи определяется по нажатому пункту контекстного меню;
  • Скомпилируйте и запустите приложение.



Комментарии (6)
  • mihadov 11.06.2015

    Симпотично 🙂

    Могу предложить решение уведомлений внутри апликации основанное на NOTY и SignalR

  • VorsinEO 11.06.2015

    Предложите пожалуйста, как раз находимся в поиске решения по уведомлениям 🙂

  • OMI 11.06.2015

    Добрый день.

    У меня что-то ругается на using…

    —- Error 1 The type or namespace name ‘Xrm’ does not exist in the namespace ‘Microsoft’ (are you missing an assembly reference?)
    —- Error 5 The type or namespace name ‘ServiceModel’ does not exist in the namespace ‘System’ (are you missing an assembly reference?)
    И т.д.

    Подскажите, что не так?

  • slivka_83 11.06.2015

    Доброе время суток 🙂
    Нужно подключить соответствующие сборки к проетку.

  • Nymdar 11.06.2015

    Работает ли на 2011?

  • slivka_83 11.06.2015

    Концепция работать будет 🙂 А данная конкретная реализацию нужно проверять. Возможно незначительную часть кода придется подкорректировать.

*

code