jueves, 22 de junio de 2017

Notificaciones WPF para todos





En los tiempos que corren, todos los sistemas operativos e incluso los navegadores, contienen un sistema de notificaciones. Esta es una característica muy práctica y nos habilita la posibilidad de tener aplicaciones 100% conectadas y 100% vivas. Con las notificaciones, podemos tener información referente a otras aplicaciones de nuestro entorno, a usuarios, errores, etc., en el mismo momento en el que ocurren.









SignalR es una librería para desarrolladores que hace fácil la característica y funcionalidad del tiempo real con unos resultados fantásticos.
He desarrollado una solución .NET completa de notificaciones propias para nuestros sistemas. La solución nos brinda la posibilidad de añadir fácilmente un sistema de notificaciones a nuestros entornos.
Ejemplo gráfico de la solución:












































El projecto es completamente de código abierto y lo tenéis disponible en mi repositorio de Git Hub.

La solución contiene 4 importantes proyectos:



  • MLNotification.Service.- Es un servicio selfhosting que contiene la funcionalidad de la comunicación entre procesos. Está aplicación debería ser instalada (Convertida en servicio Windows normalmente) en un servidor remoto, publicado en una ip pública. Las demás aplicaciones satélite se conectarán a este servicio e enviarán y recibirán mensajes a través de él.
  • MLNotification.WPFClient.- Es la aplicación cliente principal conectada al servicio. Esta aplicación debe ser instalada en todas las máquinas de usuarios y mostrará todas las notificaciones en tiempo real.
  • MLNotification.ServiceClientConnect.- Es un proyecto que envuelve la funcionalidad de conexión entre los clientes y el servicio, para hacer más fácil su consumo. Además transforma las características dinámicas de SignalR en tipadas, para un mejor control en tiempo de compilación.
  • MLNotification.Domain.- Comparte los principales tipos de datos entre todos los proyectos.


Los otros proyectos son simplemente proyectos de pruebas y testeo.



Proyecto Service


Es un clásico servicio selfhosting de SignalR.

It is a classical self hosting SignalR service y sus principales clases son:

A class inherit of Hub:


using Microsoft.AspNet.SignalR;
using MLNotification.Domain;
using System;
using System.Threading.Tasks;

namespace MLNotification.Service
{
    public class MLMessageHub : Hub
    {
        async public override Task OnConnected()
        {
            // Code for connection to service.
        }
        async public override Task OnDisconnected(bool stopCalled)
        {
            Console.WriteLine("Nueva conexion con Id=" + Context.ConnectionId);

            var message = new NotificationMessage
            {
                Subject     = "New service desconnection",
                Body        = $"There is a desconnection from the UserId:{Context.ConnectionId}",
                MessageDate = DateTime.Now,
                MessageType = MessageType.Information,
                UriImage    = "http://www.tampabay.com/resources/images/dti/rendered/2015/04/wek_plug041615_15029753_8col.jpg"
            };

            await Clients.Caller.ProcessMessage(message);
            await Clients.Others.ProcessMessage(message);
        }

        async public Task SendMessage(NotificationMessage message)
        {
            Console.WriteLine("[" + message.User + "]: " + message.Body);
            await Clients.All.ProcessMessage(message);
        }


        async public Task RegisterUser(UserInfo userInfo)
        {
            // Code for register user
        }

    }
}

Como veremos más adelante, una clase envoltorio dentro del Proyecto MLNotifications.ServiceClientConnect nos facilitará el trabajo de conexión con el servidor.


Program.cs:


using Microsoft.Owin.Hosting;
using System;

namespace MLNotification.Service
{
    class Program
    {
        static void Main(string[] args)
        {
            using (WebApp.Start<Startup>("http://localhost:11111"))
            {
                Console.WriteLine("Hub on http://localhost:11111");
                Console.ReadLine();
            }
        }
    }
}


Startup.cs:


using Microsoft.Owin.Cors;
using Owin;

namespace MLNotification.Service
{
    public class Startup
    {
        public static void Configuration(IAppBuilder app)
        {
            app.UseCors(CorsOptions.AllowAll);
            app.MapSignalR();
        }
    }
}



Client

Es una aplicación de WPF con MVVM y está conectada al servicio en todo momento. Tiene 2 partes principales:




Estas partes son el NotifyIcon + ToolTip.

El NotifyIcon, muestra el popup the mensajes de notificación (por defecto si el grid de notificación está cerrado, aunque esto puede ser configurado) y abre la superficie del grid de notificaciones. Tiene un menú contextual con dos opciones, la primera es un acceso directo a la configuración y la segunda cierra la aplicación. Para esta parte he usado la fantástica librería WPF NotifyIcon de el desarrollador Philipp Summi, que es mucho más que recomendable.

La segunda parte es el grid de notificaciones:




El grid de notificaciones, es un repositorio en donde se muestran los mensajes de notificación. Los mensajes de notificación pueden ser cerrados de manera individual o limpiados todos a la vez. También tenemos un acceso a la configuración del programa.


Tipos de Notificaciones

Hay 8 tipos importantes de notificaciones y se dividen en 2 grupos: menos importantes (simples) y más importantes (urgentes). Dentro de cada grupo hay 4 tipos: Información, warning, error y muy importante.




































































































MLNotification.WPFClient código del proyecto








































El Proyecto de WPF contiene 4 grupos. La imagen habla por sí misma y es auto descriptiva Para más información tenéis el código del proyecto.




Configuración

































En la ventana de configuración, tendremos disponibles 2 partes fundamentales:
  • Service
    • Service Address .- Es la dirección http donde se expondrá el servicio.
  • Balloon Messages
    • Visibility Time (Seconds) .- Esta opción mostrará el número de segundos que se mantendrá visible el mensaje balloon.
    • Show Balloon with notifications open .- Si está activada, siempre se mostrará el mensaje balloon, aunque el grid de notificaciones esté abierto, si está desactivada, el mensaje balloon solo se mostrará en caso de estar cerrado el grid de notificaiones.

La información de la configuración se salva en el Isolated Sotrage del usuario, y el servicio detecta si se ha modificado la dirección y vuelve a conectar en ese caso.

Si añadimos una dirección no correcta o en la cual no esté escuchando ningún servicio, se mostrará un error:



































Clase NotificationMessage

La clase NotificationMessage es muy importante dentro de la solución. Los objetos de esta clase viajan a través del servicio a los clientes y viceversa, además contiene la información del mensaje.


using System;
using System.ComponentModel.DataAnnotations;

namespace MLNotification.Domain
{
    [Serializable]
    public class NotificationMessage : INotificationMessage
    {
        [Required]
        [MaxLength(100)]
        public string Subject { get; set; }

        [Required]
        [MaxLength(2000)]
        public string Body { get; set; }

        public MessageType MessageType { get; set; }

        [MaxLength(50)]
        public string Group { get; set; }

        [MaxLength(50)]
        public string User { get; set; }

        [MaxLength(50)]
        public string Server { get; set; }

        public DateTime MessageDate { get; set; }

        public string UriImage { get; set; }
    }

}
namespace MLNotification.Domain
{
    public enum MessageType
    {
        Information,
        Warnnig,
        Error, 
        VeryImportant,
        Information_urgent,
        Warnnig_urgent,
        Error_urgent,
        VeryImportant_urgent
    }
}



Sus propiedades se explican por si solas:



MLNotification.ServiceClientConnect

Este Proyecto es un envoltorio que facilita la comunicación entre el servicio y el cliente.












MLMessageHubConect es su clase principal: 


using Microsoft.AspNet.SignalR.Client;
using MLNotification.Domain;
using MLNotification.ServiceClientConnect.EventArgs;
using System;
using System.Threading.Tasks;

namespace MLNotification.ServiceClientConnect
{
    public class MLMessageHubConect : IDisposable, IMLMessageHubConect
    {
        public  HubConnection conexionHub = null;
        private IHubProxy     proxyHub    = null;

        public IUserInfo userInfo;

        private const string NotificationMessageStr = "ProcessMessage";
        private const string SendMessageStr         = "SendMessage";
        private const string RegisterUserStr        = "RegisterUser";

        public event EventHandler<MLMessageEventArgs> ProcessMessage;



        public MLMessageHubConect(HubConnection conexionHub, IHubProxy proxyHub, IUserInfo userInfo = null)
        {
            this.conexionHub = conexionHub;
            this.proxyHub    = proxyHub;

            this.userInfo = userInfo;

            Connect();

            RegisterUser(userInfo);
        }

        private void Connect()
        {
            try
            {
                proxyHub.On(NotificationMessageStr, (NotificationMessage message) =>
                {
                    if (message != null && conexionHub != null)
                    {
                        OnProcessMessage(message);
                    }
                });

                Task.WaitAll(conexionHub.Start());

                RegisterUser(userInfo);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine("Error " + ex.Message);

                throw new HubException($"Error to connect to Service. Check the service is online, and the ServiceAddress is correct. Error:{ex.Message}");
            }
        }


        public Task SendMessage(NotificationMessage message)
        {
            return proxyHub.Invoke(SendMessageStr, message);
        }

        public Task RegisterUser(IUserInfo userInfo)
        {
            return proxyHub.Invoke(RegisterUserStr, userInfo);
        }

        protected internal virtual void OnProcessMessage(NotificationMessage message) => ProcessMessage?.Invoke(this, new MLMessageEventArgs(message));


        public void Dispose()
        {
            conexionHub.Dispose();
            conexionHub = null;
            proxyHub    = null;
        }
    }
}


Esta clase contiene.

Campos y propiedades:
  • conexionHub y proxyHub .- Objectos injectados al servicio SignalR.
  • userInfo .- Almacena la información de la session del usuario.
  • NotificationMessageStr, SendMessageStr and RegisterUserStr.- Estos contienen la información en campos const de los nombres a los métodos dinámicos del hub de SignalR.

Eventos:
  • ProcessMessage .- Este evento se lanza cuando el servicio nos comunica mensajes entrantes.

Metodos:
  • Conect .- Se conecta con el servicio SignalR y habilita la entrada de mensajes.
  • SendMessage .- Envía un mensaje al servidor SignalR.
  • RegisterUser .- Registra un usuario en el SignalR.



Haciendo un cliente WPF

Vamos a hacer una aplicación WPF que conecte con el servicio de consola SignalR y envíe mensajes personalizados. Para mejorar su aspecto gráfico, nos apoyaremos en la fantástica librería MahApps.

Instalaremos el paquete de nuget Microsoft.AspNet.SignalR.Client:







Añadiremos referencias a MLNotifications.Domain y MLNotifications.ServiceClientConnect.

En la parte del XAML de la MainWindow, crearemos una ventana simple con cajas de texto para las principales propiedades de la clase MLNotification.Domain.NotificationMessage.
































CodeBehind:

Crearemos un campo privado para la clase envoltorio de conexión con el hub de SignalR.


private MLMessageHubConect connectHub;

In el evento Loaded, conectaremos con el service de consola SignalR.


private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    try
    {
        if (connectHub != null) connectHub.Dispose();

        connectHub = BuilderMLMessageHubConnect.CreateMLMessageHub(txtUrl.Text);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}

En el evento click del botón enviar, enviaremos un message al servicio.


async private void btnSend_Click(object sender, RoutedEventArgs e)
{
    var message = new NotificationMessage
    {
        Subject     = txtSubject.Text,
        Body        = txtMensaje.Text,
        User        = txtUser.Text,
        MessageDate = DateTime.Now,
        Server      = txtServer.Text,
        UriImage    = txtUriImage.Text
    };

    message.MessageType = (MessageType)cmbType.SelectedIndex;

    await connectHub.SendMessage(message);
}

Este es todo el código:


using MahApps.Metro.Controls;
using MLNotification.Domain;
using MLNotification.ServiceClientConnect;
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;


namespace MLNotification.WPClient2
{

    public partial class MainWindow : MetroWindow
    {

        private MLMessageHubConect connectHub;



        public MainWindow()
        {
            InitializeComponent();

            this.AllowsTransparency = true;

            /// enabled drag and drop the window
            MouseDown += (sender, e) =>
            {
                this.DragMove();
                e.Handled = false;
            };

            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            try
            {
                if (connectHub != null) connectHub.Dispose();

                connectHub = BuilderMLMessageHubConnect.CreateMLMessageHub(txtUrl.Text);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }


        async private void btnEnviar_Click(object sender, RoutedEventArgs e)
        {
            var message = new NotificationMessage
            {
                Subject     = txtSubject.Text,
                Body        = txtMensaje.Text,
                User        = txtUser.Text,
                MessageDate = DateTime.Now,
                Server      = txtServer.Text,
                UriImage    = txtUriImage.Text
            };

            message.MessageType = (MessageType)cmbType.SelectedIndex;

            await connectHub.SendMessage(message);
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            connectHub.Dispose();

            base.OnClosing(e);
        }


        private void ButtonClose_Click(object sender, RoutedEventArgs e) => Close();

        private void txtUriImage_TextChanged(object sender, TextChangedEventArgs e)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(txtUriImage?.Text)) return;

                BitmapImage logo = new BitmapImage();
                logo.BeginInit();
                string path = txtUriImage.Text;
                logo.UriSource = new Uri(path, UriKind.RelativeOrAbsolute);
                logo.EndInit();

                var img = new ImageBrush(logo);

                bdImage.Background = img;
            }
            catch (Exception)
            {
                // nothing
            }
        }
    }


}








Para más información chequear el proyecto MLNotification.WPFClient2 en la solución.
La solución también contiene un Proyecto de cliente de consola.

AÑADIENDO FUNCIONALIDAD DE ESCUCHA DE MENSAJES A NUESTRA APP.

Dentro de nuestro Proyecto WPF cliente, podemos añaidir la funcionalidad de escucha de notificaciones. Para esto añadiremos un Listbox y completaremos el evento Loaded:


private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    try
    {
        if (connectHub != null) connectHub.Dispose();

        connectHub = BuilderMLMessageHubConnect.CreateMLMessageHub(txtUrl.Text);

        connectHub.ProcessMessage += (sender2, e2) => lstServerMessages.Dispatcher.Invoke(() =>
        {
            lstServerMessages.Items.Add(e2.NotificationMessage.Body);
        }, System.Windows.Threading.DispatcherPriority.Background);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}