Example T1024676
Visible to All Users

WinForms Chat Client Demo (DXHtmlMessenger)

DXHtmlMessenger is a Windows Forms demo that emulates a messenger app. It shows how to use DevExpress WinForms controls to build a desktop UI from HTML and CSS.

overview.png

Files to Look At

Model

Data Layer

User Interface

Desktop UI based on HTML and CSS

DXHtmlMessenger is built based on the DevExpress controls that render a UI from HTML and CSS. The image below demonstrates the main form.

main form parts

The form's GUI is set up using the following DevExpress controls:

  • HtmlContentControl — Renders a UI from an HTML-CSS template. Can show data from a bound data context (a business object or data source item). The HTML code can contain the input tag, which defines a placeholder for any external control (for instance, a text box).
  • Data Grid's TileView — Allows you to render its tiles from a specific template in HTML format, and apply CSS styles to tile elements.
  • Data Grid's ItemsView — Presents records from a data source as an item list. Each list item is rendered using an HTML-CSS template.

The app demonstrates the HtmlContentPopup component used to show specific information in popup windows (information about users and popup menus). This component also renders a UI from an HTML-CSS template. It can also show data from a bound data context (a business object or data source item).

popup-window

You can find the HTML code and CSS styles from which controls render UIs in the project's Assets/HTML and Assets/CSS folders.

HTML-based UI rendering supports DirectX hardware acceleration. See the following topic to learn how to enable the DirectX engine: DirectX Hardware Acceleration.

See the following page for more information on how the application's UI is built: Application UI Design

Application Layers

The app uses the MVVM pattern to separate the code into layers — Data Layer, Model, and View (UI). This separation grants you multiple benefits, such as a more independent development process for both developers and designers, easier code testing, and simpler UI redesigns. The data layer uses a set of interfaces to interact with the Model and UI layers.

The example shows how to supply data to Views, handle user actions, and interact with the server (send and listen to commands). The current implementation of the data layer is in-memory storage that gets data from the sample DevAV database.
To communicate with any messenger app, you can replace the current implementation of the data layer with your own implementation. Modification of other layers is required if you want to extend text messaging with advanced features, like image and video support, calls, group chats, etc.

See the following page for more information: Work with data

Documentation

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

Docs/CS/Model/Contact.cs
C#
using System; using System.Collections.Concurrent; using System.ComponentModel; using System.Drawing; namespace DevExpress.DevAV.Chat.Model { public class Contact { [Browsable(false)] public long ID { get; } public string UserName { get; } public Image Avatar { get; } [Browsable(false)] public DateTime LastOnline { get; } public string LastOnlineText { get; } public int UnreadCount { get; } [Browsable(false)] public bool HasUnreadMessages { get; } public bool IsInactive { get; } } }
Docs/VB/Model/Contact.vb
Visual Basic
Imports System Imports System.Collections.Concurrent Imports System.ComponentModel Imports System.Drawing Namespace DevExpress.DevAV.Chat.Model Public Class Contact <Browsable(False)> Public ReadOnly Property ID() As Long Public ReadOnly Property UserName() As String Public ReadOnly Property Avatar() As Image <Browsable(False)> Public ReadOnly Property LastOnline() As DateTime Public ReadOnly Property LastOnlineText() As String Public ReadOnly Property UnreadCount() As Integer <Browsable(False)> Public ReadOnly Property HasUnreadMessages() As Boolean Public ReadOnly Property IsInactive() As Boolean End Class End Namespace
Docs/CS/Model/Message.cs
C#
using System; using System.ComponentModel; namespace DevExpress.DevAV.Chat.Model { public class Message { [Browsable(false)] public long ID { get; } public Contact Owner { get; } public string Text { get; } public DateTime SentOrUpdated { get; } public string StatusText { get; } [Browsable(false)] public bool IsEdited { get; } [Browsable(false)] public bool IsDeleted { get; } [Browsable(false)] public bool IsLiked { get; } [Browsable(false)] public bool IsOwnMessage { get; } [Browsable(false)] public bool IsFirstMessageOfReply { get; } [Browsable(false)] public bool IsFirstMessageOfBlock { get; } } }
Docs/VB/Model/Message.vb
Visual Basic
Imports System Imports System.ComponentModel Namespace DevExpress.DevAV.Chat.Model Public Class Message <Browsable(False)> Public ReadOnly Property ID() As Long Public ReadOnly Property Owner() As Contact Public ReadOnly Property Text() As String Public ReadOnly Property SentOrUpdated() As DateTime Public ReadOnly Property StatusText() As String <Browsable(False)> Public ReadOnly Property IsEdited() As Boolean <Browsable(False)> Public ReadOnly Property IsDeleted() As Boolean <Browsable(False)> Public ReadOnly Property IsLiked() As Boolean <Browsable(False)> Public ReadOnly Property IsOwnMessage() As Boolean <Browsable(False)> Public ReadOnly Property IsFirstMessageOfReply() As Boolean <Browsable(False)> Public ReadOnly Property IsFirstMessageOfBlock() As Boolean End Class End Namespace
Docs/CS/Data/IMessageServer.cs
C#
using System; using System.Collections.Generic; using System.Threading.Tasks; using DevExpress.DevAV.Chat.Commands; using DevExpress.DevAV.Chat.Events; using DevExpress.DevAV.Chat.Model; namespace DevExpress.DevAV.Chat { public interface IMessageServer { Task<IChannel> Connect(string userName); } public interface IChannel : IDisposable { // Common void Subscribe(Action<ChannelEvent> onEvent); string UserName { get; } void Send(ChannelCommand command); // Contacts void Subscribe(Action<Dictionary<long, ContactEvent>> onEvents); Task<UserInfo> GetUserInfo(string userName); Task<UserInfo> GetUserInfo(long id); Task<IReadOnlyCollection<Contact>> GetContacts(); void Send(ContactCommand command); // Messages void Subscribe(Action<Dictionary<long, MessageEvent>> onEvents); Task<IReadOnlyCollection<Message>> GetHistory(Contact contact); void Send(MessageCommand command); } }
Docs/VB/Data/IMessageServer.vb
Visual Basic
Imports System Imports System.Collections.Generic Imports System.Threading.Tasks Imports DevExpress.DevAV.Chat.Commands Imports DevExpress.DevAV.Chat.Events Imports DevExpress.DevAV.Chat.Model Namespace DevExpress.DevAV.Chat Public Interface IMessageServer Function Connect(ByVal userName As String) As Task(Of IChannel) End Interface Public Interface IChannel Inherits IDisposable ' Common Sub Subscribe(ByVal onEvent As Action(Of ChannelEvent)) ReadOnly Property UserName() As String Sub Send(ByVal command As ChannelCommand) ' Contacts Sub Subscribe(ByVal onEvents As Action(Of Dictionary(Of Long, ContactEvent))) Function GetUserInfo(ByVal userName As String) As Task(Of UserInfo) Function GetUserInfo(ByVal id As Long) As Task(Of UserInfo) Function GetContacts() As Task(Of IReadOnlyCollection(Of Contact)) Sub Send(ByVal command As ContactCommand) ' Messages Sub Subscribe(ByVal onEvents As Action(Of Dictionary(Of Long, MessageEvent))) Function GetHistory(ByVal contact As Contact) As Task(Of IReadOnlyCollection(Of Message)) Sub Send(ByVal command As MessageCommand) End Interface End Namespace
Docs/CS/Data/Events/ChannelEvent.cs
C#
using System; using System.ComponentModel; using System.Threading.Tasks; namespace DevExpress.DevAV.Chat.Events { public abstract class ChannelEvent { protected ChannelEvent(IChannel channel) { this.Channel = channel; } public IChannel Channel { get; } public string UserName { get { return Channel.UserName; } } } // public class CredentialsRequiredEvent : ChannelEvent { public string Salt { get; } public void SetAccessTokenQuery(Task<string> query) { /* some code */ } [EditorBrowsable(EditorBrowsableState.Never)] public Task<string> GetAccessTokenQuery() { /* some code*/ } } public class ChannelReadyEvent : ChannelEvent { public ChannelReadyEvent(IChannel channel) : base(channel) { } } }
Docs/VB/Data/Events/ChannelEvent.vb
Visual Basic
Imports System Imports System.ComponentModel Imports System.Threading.Tasks Namespace DevExpress.DevAV.Chat.Events Public MustInherit Class ChannelEvent Protected Sub New(ByVal channel As IChannel) Me.Channel = channel End Sub Public ReadOnly Property Channel() As IChannel Public ReadOnly Property UserName() As String Get Return Channel.UserName End Get End Property End Class ' Public Class CredentialsRequiredEvent Inherits ChannelEvent Public ReadOnly Property Salt() As String Public Sub SetAccessTokenQuery(ByVal query As Task(Of String)) ' some code End Sub <EditorBrowsable(EditorBrowsableState.Never)> Public Function GetAccessTokenQuery() As Task(Of String) ' some code End Function End Class Public Class ChannelReadyEvent Inherits ChannelEvent Public Sub New(ByVal channel As IChannel) MyBase.New(channel) End Sub End Class End Namespace
Docs/CS/Data/Events/ContactEvents.cs
C#
using System; using DevExpress.DevAV.Chat.Model; namespace DevExpress.DevAV.Chat.Events { public abstract class ContactEvent { public long Id { get; } public abstract void Apply(Contact contact); } // public class StatusChanged : ContactEvent { public Contact.Status Status { get; } public DateTime LastOnline { get; } public override void Apply(Contact contact) { contact.StatusCore = Status; contact.LastOnline = Status == Contact.Status.Inactive ? LastOnline : DateTime.MinValue; } } public class UnreadChanged : ContactEvent { public int UnreadCount { get; } public override void Apply(Contact contact) { contact.UnreadCount += UnreadCount; } } public class AllMessagesRead : ContactEvent { public AllMessagesRead(long id) : base(id) { } public override void Apply(Contact contact) { contact.UnreadCount = 0; } } public class NewMessages : ContactEvent { public NewMessages(long id) : base(id) { } public override void Apply(Contact entity) { /* do nothing */ } } }
Docs/VB/Data/Events/ContactEvents.vb
Visual Basic
Imports System Imports DevExpress.DevAV.Chat.Model Namespace DevExpress.DevAV.Chat.Events Public MustInherit Class ContactEvent Public ReadOnly Property Id() As Long Public MustOverride Sub Apply(ByVal contact As Contact) End Class ' Public Class StatusChanged Inherits ContactEvent Public ReadOnly Property Status() As Contact.Status Public ReadOnly Property LastOnline() As DateTime Public Overrides Sub Apply(ByVal contact As Contact) contact.StatusCore = Status contact.LastOnline = If(Status = Contact.Status.Inactive, LastOnline, DateTime.MinValue) End Sub End Class Public Class UnreadChanged Inherits ContactEvent Public ReadOnly Property UnreadCount() As Integer Public Overrides Sub Apply(ByVal contact As Contact) contact.UnreadCount += UnreadCount End Sub End Class Public Class AllMessagesRead Inherits ContactEvent Public Sub New(ByVal id As Long) MyBase.New(id) End Sub Public Overrides Sub Apply(ByVal contact As Contact) contact.UnreadCount = 0 End Sub End Class Public Class NewMessages Inherits ContactEvent Public Sub New(ByVal id As Long) MyBase.New(id) End Sub Public Overrides Sub Apply(ByVal entity As Contact) ' do nothing End Sub End Class End Namespace
Docs/CS/Data/Events/MessageEvents.cs
C#
using DevExpress.DevAV.Chat.Model; namespace DevExpress.DevAV.Chat.Events { public abstract class MessageEvent { public long Id { get; } public virtual void Apply(Message entity) { /* do nothing */ } } // public class MessageDeleted : MessageEvent { public override void Apply(Message entity) { entity.SentOrUpdated = System.DateTime.Now; entity.Text = string.Empty; entity.MarkAsDeleted(); } } public class MessageLiked : MessageEvent { public override void Apply(Message entity) { entity.MarkAsLiked(); } } public class MessageTextChanged : MessageEvent { public string Text { get; } public override void Apply(Message entity) { entity.SentOrUpdated = System.DateTime.Now; entity.Text = Text; entity.MarkAsEdited(); } } }
Docs/VB/Data/Events/MessageEvents.vb
Visual Basic
Imports DevExpress.DevAV.Chat.Model Namespace DevExpress.DevAV.Chat.Events Public MustInherit Class MessageEvent Public ReadOnly Property Id() As Long Public Overridable Sub Apply(ByVal entity As Message) ' do nothing End Sub End Class ' Public Class MessageDeleted Inherits MessageEvent Public Overrides Sub Apply(ByVal entity As Message) entity.SentOrUpdated = DateTime.Now entity.Text = String.Empty entity.MarkAsDeleted() End Sub End Class Public Class MessageLiked Inherits MessageEvent Public Overrides Sub Apply(ByVal entity As Message) entity.MarkAsLiked() End Sub End Class Public Class MessageTextChanged Inherits MessageEvent Public ReadOnly Property Text() As String Public Overrides Sub Apply(ByVal entity As Message) entity.SentOrUpdated = DateTime.Now entity.Text = Text entity.MarkAsEdited() End Sub End Class End Namespace
Docs/CS/Data/Commands/ChannelCommands.cs
C#
namespace DevExpress.DevAV.Chat.Commands { public abstract class ChannelCommand { public IChannel Channel { get; } } // public class LogOff : ChannelCommand { public LogOff(IChannel channel) : base(channel) { } } }
Docs/VB/Data/Commands/ChannelCommands.vb
Visual Basic
Namespace DevExpress.DevAV.Chat.Commands Public MustInherit Class ChannelCommand Public ReadOnly Property Channel() As IChannel End Class ' Public Class LogOff Inherits ChannelCommand Public Sub New(ByVal channel As IChannel) MyBase.New(channel) End Sub End Class End Namespace
Docs/CS/Data/Commands/ContactCommands.cs
C#
using System; using DevExpress.DevAV.Chat.Model; namespace DevExpress.DevAV.Chat.Commands { public abstract class ContactCommand { public Contact Contact { get; } } // public class AddMessage : ContactCommand { public AddMessage(Contact contact) : base(contact) { } public string Message { get; } public DateTime Sent { get; } } public class ReadMessages : ContactCommand { public ReadMessages(Contact contact) : base(contact) { } } }
Docs/VB/Data/Commands/ContactCommands.vb
Visual Basic
Imports System Imports DevExpress.DevAV.Chat.Model Namespace DevExpress.DevAV.Chat.Commands Public MustInherit Class ContactCommand Public ReadOnly Property Contact() As Contact End Class ' Public Class AddMessage Inherits ContactCommand Public Sub New(ByVal contact As Contact) MyBase.New(contact) End Sub Public ReadOnly Property Message() As String Public ReadOnly Property Sent() As DateTime End Class Public Class ReadMessages Inherits ContactCommand Public Sub New(ByVal contact As Contact) MyBase.New(contact) End Sub End Class End Namespace
Docs/CS/Data/Commands/MessageCommands.cs
C#
namespace DevExpress.DevAV.Chat.Commands { public abstract class MessageCommand { public long MessageId { get; } } // public class DeleteMessage : MessageCommand { public DeleteMessage(long messageID) : base(messageID) { } } public class LikeMessage : MessageCommand { public LikeMessage(long messageID) : base(messageID) { } } }
Docs/VB/Data/Commands/MessageCommands.vb
Visual Basic
Namespace DevExpress.DevAV.Chat.Commands Public MustInherit Class MessageCommand Public ReadOnly Property MessageId() As Long End Class ' Public Class DeleteMessage Inherits MessageCommand Public Sub New(ByVal messageID As Long) MyBase.New(messageID) End Sub End Class Public Class LikeMessage Inherits MessageCommand Public Sub New(ByVal messageID As Long) MyBase.New(messageID) End Sub End Class End Namespace
Messenger.cs(vb)
C#
namespace DXHtmlMessengerSample { using System; using System.Drawing; using DevExpress.LookAndFeel; using DevExpress.Utils.MVVM; using DevExpress.Utils.MVVM.Services; using DevExpress.XtraBars.Docking2010.Customization; using DevExpress.XtraBars.Docking2010.Views.WindowsUI; using DevExpress.XtraBars.ToolbarForm; using DevExpress.XtraEditors; using DXHtmlMessengerSample.ViewModels; using DXHtmlMessengerSample.Views; public partial class MessengerForm : ToolbarForm { public MessengerForm() { InitializeComponent(); if(!mvvmContext.IsDesignMode) { InitializeStyles(); InitializeNavigation(); InitializeBindings(); } } void InitializeStyles() { darkThemeBBI.ImageOptions.SvgImage = DXHtmlMessenger.SvgImages["DarkTheme"]; Styles.ContactInfo.Apply(contactInfoPopup); Styles.UserInfo.Apply(userInfoPopup); } void InitializeNavigation() { // Flyout Service for all child views var flyoutService = WindowedDocumentManagerService.CreateFlyoutFormService(); flyoutService.FormStyle = (form) => { var flyout = form as FlyoutDialog; flyout.CornerRadius = new DevExpress.Utils.Drawing.CornerRadius(8); flyout.Properties.Style = FlyoutStyle.Popup; flyout.Properties.Appearance.BorderColor = Color.FromArgb(0x66, Color.Black); }; mvvmContext.RegisterDefaultService("Flyout", flyoutService); // Window Service for showing user info var userInfoDialog = userInfoPopup.CreateWindowService(); userInfoDialog.ShowMode = WindowService.WindowShowMode.Modal; userInfoDialog.WindowStyle = (window) => { var popup = window as IPopupWindow; popup.PopupSize = new Size(516, 306); popup.DestroyOnHide = false; }; mvvmContext.RegisterDefaultService("UserInfoDialog", userInfoDialog); // Window Service for showing contact info var contactInfoFlyout = contactInfoPopup.CreateWindowService(); contactInfoFlyout.WindowStyle = (window) => { var popup = window as IPopupWindow; popup.PopupSize = new Size(368, 374); }; mvvmContext.RegisterDefaultService("ContactInfoFlyout", contactInfoFlyout); } void InitializeBindings() { var fluent = mvvmContext.OfType<MessengerViewModel>(); // Bind life-cycle events fluent.WithEvent(this, nameof(Load)) .EventToCommand(x => x.OnLoad); fluent.WithEvent(this, nameof(FormClosed)) .EventToCommand(x => x.OnClosed); // Bind application title fluent.SetBinding(this, x => x.Text, x => x.Title); } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); var fluent = mvvmContext.OfType<MessengerViewModel>(); // Set the relationship with child views and their ViewModels var viewModel = fluent.ViewModel; MVVMContext.SetParentViewModel(this.contactsView, viewModel); MVVMContext.SetParentViewModel(this.messagesView, viewModel); } void userInfoPopup_ViewModelSet(object sender, ViewModelSetEventArgs e) { var fluent = userInfoPopup.OfType<UserViewModel>(); fluent.BindCommand("lnkLogOff", x => x.LogOff); fluent.BindCommand("btnClose", x => x.Close); } void contactInfoPopup_ViewModelSet(object sender, ViewModelSetEventArgs e) { var fluent = contactInfoPopup.OfType<ContactViewModel>(); fluent.BindCommand("lnkEmail", x => x.MailTo); fluent.BindCommand("btnPhoneCall", x => x.PhoneCall); fluent.BindCommand("btnVideoCall", x => x.VideoCall); fluent.BindCommand("btnMessage", x => x.TextMessage); } bool isDarkTheme; void OnDarkThemeClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) { var palette = (isDarkTheme = !isDarkTheme) ? SkinSvgPalette.Bezier.ArtHouse : SkinSvgPalette.Bezier.Default; WindowsFormsSettings.DefaultLookAndFeel.SetSkinStyle(SkinStyle.Bezier, palette); } sealed class Styles { public static Style ContactInfo = new ContactInfoStyle(); public static Style UserInfo = new UserInfoStyle(); // sealed class ContactInfoStyle : Style { } sealed class UserInfoStyle : Style { } } } }
ViewModels/MessengerViewModel.cs(vb)
C#
namespace DXHtmlMessengerSample.ViewModels { using System.Threading.Tasks; using DevExpress.DevAV.Chat; using DevExpress.DevAV.Chat.Commands; using DevExpress.DevAV.Chat.Events; using DevExpress.DevAV.Chat.Model; using DevExpress.Mvvm; using DevExpress.Mvvm.DataAnnotations; using DevExpress.Mvvm.POCO; using DXHtmlMessengerSample.Services; public class MessengerViewModel { public virtual string Title { get; protected set; } IDispatcherService dispatcher; IChannel channel; public async Task OnLoad() { dispatcher = this.GetRequiredService<IDispatcherService>(); // Load settings and initialize var settingsService = this.GetRequiredService<ISettingsService>(); var theme = settingsService.Theme ?? "Light"; var currentUser = settingsService.CurrentUser ?? "John Heart"; // Open messenger channel var messageServer = this.GetRequiredService<IMessageServer>(); channel = await messageServer.Create(currentUser); channel.Subscribe(OnChannelEvent); // Pass the channel into dependent ViewModels Messenger.Default.Send(channel); await dispatcher.BeginInvoke(() => Title = $"DX HTML MESSENGER (CS) - [{currentUser.ToUpper()}]"); } int authCounter = 0; void OnChannelEvent(ChannelEvent @event) { var credentialsRequired = @event as CredentialsRequiredEvent; if(credentialsRequired != null) { if(0 == authCounter++) { // provide the access token from local cache without interaction var cacheQuery = QueryAccessTokenFromLocalAuthCache(@event.UserName, credentialsRequired.Salt); credentialsRequired.SetAccessTokenQuery(cacheQuery); } else { // or query access-token asynchronously for the specific user var userQuery = QueryAccessTokenFromUser(@event.UserName, credentialsRequired.Salt); credentialsRequired.SetAccessTokenQuery(userQuery); } } } Task<string> QueryAccessTokenFromLocalAuthCache(string userName, string salt) { // simple emulation of local auth cache return Task.FromResult(DevAVEmpployeesInMemoryServer.GetPasswordHash(string.Empty, salt)); } Task<string> QueryAccessTokenFromUser(string userName, string salt) { var accessTokenQueryCompletionSource = new TaskCompletionSource<string>(); dispatcher.BeginInvoke(() => { var signInViewModel = SignInViewModel.Create(userName, salt); signInViewModel.ShowDialog(); if(!string.IsNullOrEmpty(signInViewModel.AccessToken)) accessTokenQueryCompletionSource.SetResult(signInViewModel.AccessToken); else accessTokenQueryCompletionSource.SetCanceled(); }); return accessTokenQueryCompletionSource.Task; } public void OnClosed() { if(channel != null) channel.Dispose(); channel = null; } public void LogOff() { if(channel != null) channel.Send(new LogOff(channel)); } UserViewModel userViewModel; [Command(isCommand: false)] public void ShowUserInfo(UserInfo userInfo) { ShowPopup(userViewModel ?? (userViewModel = UserViewModel.Create(userInfo)), userInfo); } ContactViewModel contactViewModel; [Command(isCommand: false)] public void ShowContactInfo(UserInfo contactInfo) { ShowPopup(contactViewModel ?? (contactViewModel = ContactViewModel.Create(contactInfo)), contactInfo); } void ShowPopup(UserInfoViewModel viewModel, UserInfo info) { viewModel.SetParentViewModel(this); var popup = this.GetService<IWindowService>(viewModel.ServiceKey); popup.Show(null, viewModel, info, this); } public static void ShowUserInfo(object viewModel, UserInfo userInfo) { var messenger = viewModel.GetParentViewModel<MessengerViewModel>(); if(messenger != null) messenger.ShowUserInfo(userInfo); } public static void ShowContactInfo(object viewModel, UserInfo contactInfo) { var messenger = viewModel.GetParentViewModel<MessengerViewModel>(); if(messenger != null) messenger.ShowContactInfo(contactInfo); } } }
Views/ContactsView.cs(vb)
C#
namespace DXHtmlMessengerSample.Views { using System; using System.Drawing; using System.Windows.Forms; using DevExpress.Data; using DevExpress.DevAV.Chat.Model; using DevExpress.Utils.Html; using DevExpress.XtraEditors; using DevExpress.XtraGrid.Views.Base; using DevExpress.XtraGrid.Views.Tile; using DXHtmlMessengerSample.ViewModels; public partial class ContactsView : XtraUserControl { public ContactsView() { InitializeComponent(); if(!mvvmContext.IsDesignMode) { InitializeStyles(); InitializeBindings(); InitializeBehavior(); InitializeMenusAndTooltips(); } } void InitializeStyles() { Styles.SearchPanel.Apply(searchPanel); Styles.ContactMenu.Apply(contactMenuPopup); Styles.ContactTooltip.Apply(contactTooltip); contactsTileView.HtmlImages = DXHtmlMessenger.SvgImages; } void InitializeBindings() { var fluent = mvvmContext.OfType<ContactsViewModel>(); // Bind the contacts fluent.SetBinding(gridControl, gc => gc.DataSource, x => x.Contacts); fluent.WithEvent<TileView, FocusedRowObjectChangedEventArgs>(contactsTileView, nameof(ColumnView.FocusedRowObjectChanged)) .SetBinding(x => x.SelectedContact, (args) => args.Row as Contact, (gView, entity) => gView.FocusedRowHandle = gView.FindRow(entity)); // We need update our contacts list when the ViewModel detect changes fluent.SetTrigger(x => x.Contacts, contacts => contactsTileView.RefreshData()); // Bind life-cycle events fluent.WithEvent(this, nameof(HandleCreated)) .EventToCommand(x => x.OnCreate); fluent.WithEvent(this, nameof(HandleDestroyed)) .EventToCommand(x => x.OnDestroy); // Bind context items fluent.BindCommandToElement(contactMenuPopup, "miClearConversation", x => x.ClearConversation); fluent.BindCommandToElement(contactMenuPopup, "miCopyContact", x => x.CopyContact); } void OnContactTooltipViewModelSet(object sender, DevExpress.Utils.MVVM.ViewModelSetEventArgs e) { var fluent = contactTooltip.OfType<ContactViewModel>(); fluent.BindCommand("lnkEmail", x => x.MailTo); fluent.BindCommand("btnPhoneCall", x => x.PhoneCall); fluent.BindCommand("btnVideoCall", x => x.VideoCall); fluent.BindCommand("btnMessage", x => x.TextMessage); } void InitializeBehavior() { // Setup default sorting for contacts var colLastActivity = contactsTileView.Columns["LastActivity"]; if(colLastActivity != null) contactsTileView.SortInfo.Add(colLastActivity, ColumnSortOrder.Descending); // Setup search by user name only searchControl.QueryIsSearchColumn += OnQueryIsSearchColumn; } void OnQueryIsSearchColumn(object sender, QueryIsSearchColumnEventArgs e) { e.IsSearchColumn = (e.FieldName == "UserName"); } void InitializeMenusAndTooltips() { // Bind popup menu showing/hiding contactsTileView.MouseUp += OnContactsMouseUp; contactsTileView.MouseDown += OnContactsMouseDown; // Bind tooltip showing/hiding contactsTileView.HtmlElementMouseOver += OnContactsHtmlElementMouseOver; contactsTileView.PositionChanged += OnContactsPositionChanged; contactTooltip.Hidden += ContactTooltip_Hidden; } int? activeInfoRowHandle; void ContactTooltip_Hidden(object sender, EventArgs e) { activeInfoStyle = null; if(activeInfoRowHandle.HasValue) contactsTileView.RefreshRow(activeInfoRowHandle.Value); activeInfoRowHandle = null; } CssStyle activeInfoStyle; async void OnContactsHtmlElementMouseOver(object sender, TileViewHtmlElementMouseEventArgs e) { if(e.ElementId == "info") { activeInfoStyle = e.Element.Style; activeInfoRowHandle = e.RowHandle; await System.Threading.Tasks.Task.Delay(500); if(!e.Bounds.Contains(gridControl.PointToClient(MousePosition))) return; if(activeInfoStyle != null) activeInfoStyle.SetProperty("opacity", "0.5"); var fluent = mvvmContext.OfType<ContactsViewModel>(); var tooltipViewModel = await fluent.ViewModel.EnsureTooltipViewModel(e.Row as Contact); if(!contactTooltip.IsViewModelCreated) contactTooltip.SetViewModel(typeof(ContactViewModel), tooltipViewModel); var size = ScaleDPI.ScaleSize(new Size(352, 360)); var location = new Point( e.Bounds.Right - ScaleDPI.ScaleHorizontal(6), e.Bounds.Y + ScaleDPI.ScaleHorizontal(8) - (size.Height - e.Bounds.Height) / 2); var tooltipScreenBounds = gridControl.RectangleToScreen(new Rectangle(location, size)); contactTooltip.Show(gridControl, tooltipScreenBounds); } } void OnContactsPositionChanged(object sender, EventArgs e) { contactTooltip.Hide(); } void OnContactsMouseDown(object sender, MouseEventArgs e) { var args = DevExpress.Utils.DXMouseEventArgs.GetMouseArgs(e); if(e.Button == MouseButtons.Right) { var hitInfo = contactsTileView.CalcHitInfo(e.Location); if(hitInfo.HitTest == TileControlHitTest.Item) args.Handled = true; } } void OnContactsMouseUp(object sender, MouseEventArgs e) { var args = DevExpress.Utils.DXMouseEventArgs.GetMouseArgs(e); if(e.Button == MouseButtons.Right) { var hitInfo = contactsTileView.CalcHitInfo(e.Location); if(hitInfo.HitTest == TileControlHitTest.Item) { var size = ScaleDPI.ScaleSize(new Size(212, 130)); var location = new Point(e.X - size.Width / 2, e.Y - size.Height + ScaleDPI.ScaleVertical(8)); var menuScreenBounds = gridControl.RectangleToScreen(new Rectangle(location, size)); contactMenuPopup.Show(gridControl, menuScreenBounds); args.Handled = true; } } } void OnContactItemTemplate(object sender, TileViewCustomItemTemplateEventArgs e) { Styles.Contact.Apply(e.HtmlTemplate); } void OnContactItemTemplateCustomize(object sender, TileViewItemCustomizeEventArgs e) { var contact = contactsTileView.GetRow(e.RowHandle) as Contact; if(contact != null) { var statusBadge = e.HtmlElement.FindElementById("statusBadge"); if(statusBadge != null && !contact.IsInactive) statusBadge.Style.SetBackgroundColor("@Green"); if(!contact.HasUnreadMessages) { var unreadBadge = e.HtmlElement.FindElementById("unreadBadge"); if(unreadBadge != null) unreadBadge.Hidden = true; } } } sealed class Styles { public static Style SearchPanel = new SearchPanelStyle(); public static Style Contact = new ContactStyle(); public static Style ContactMenu = new ContactMenuStyle(); public static Style ContactTooltip = new ContactTooltipStyle(); // sealed class SearchPanelStyle : Style { } sealed class ContactStyle : Style { } sealed class ContactMenuStyle : Style { public ContactMenuStyle() : base(null, "Menu") { } } sealed class ContactTooltipStyle : Style { } } } }
ViewModels/ContactsViewModel.cs(vb)
C#
namespace DXHtmlMessengerSample.ViewModels { using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DevExpress.DevAV.Chat; using DevExpress.DevAV.Chat.Commands; using DevExpress.DevAV.Chat.Events; using DevExpress.DevAV.Chat.Model; using DevExpress.Mvvm; using DevExpress.Mvvm.DataAnnotations; using DevExpress.Mvvm.POCO; public class ContactsViewModel : ChannelViewModel { public ContactsViewModel() : base() { Contacts = new Contact[0]; Messenger.Default.Register<Contact>(this, OnContact); } protected override void OnConnected(IChannel channel) { base.OnConnected(channel); channel.Subscribe(OnContactEvents); } protected override async void OnChannelReady() { var channelContacts = await Channel.GetContacts(); await DispatcherService?.BeginInvoke(() => Contacts = channelContacts); } void OnContact(Contact contact) { UpdateSelectedContact(contact); } async void OnContactEvents(Dictionary<long, ContactEvent> events) { ContactEvent @event; foreach(Contact contact in Contacts) { if(events.TryGetValue(contact.ID, out @event)) @event.Apply(contact); } if(events.Count > 0) await DispatcherService?.BeginInvoke(RaiseContactsChanged); } void RaiseContactsChanged() { this.RaisePropertyChanged(x => x.Contacts); } public virtual IReadOnlyCollection<Contact> Contacts { get; protected set; } protected void OnContactsChanged() { if(SelectedContact == null) SelectedContact = Contacts.FirstOrDefault(); else UpdateSelectedContact(SelectedContact); } public virtual Contact SelectedContact { get; set; } protected void OnSelectedContactChanged() { NotifyContactSelected(SelectedContact); this.RaiseCanExecuteChanged(x => x.ClearConversation()); this.RaiseCanExecuteChanged(x => x.CopyContact()); } int lockContact = 0; void UpdateSelectedContact(Contact contact) { if(lockContact > 0 || contact == null) return; lockContact++; try { long id = contact.ID; SelectedContact = Contacts.Where(x => x.ID == id).FirstOrDefault() ?? Contacts.FirstOrDefault(); } finally { lockContact--; } } void NotifyContactSelected(Contact contact) { if(lockContact > 0 || contact == null) return; lockContact++; try { Messenger.Default.Send(contact); } finally { lockContact--; } } public async void ShowContact(Contact contact) { var contactInfo = await Channel.GetUserInfo(contact.ID); MessengerViewModel.ShowContactInfo(this, contactInfo); } [Command(false)] public bool HasContact() { return SelectedContact != null; } [Command(CanExecuteMethodName = nameof(HasContact))] public void ClearConversation() { if(Channel != null) Channel.Send(new ClearConversation(SelectedContact)); } [Command(CanExecuteMethodName = nameof(HasContact))] public async void CopyContact() { try { var info = await Channel.GetUserInfo(SelectedContact.ID); string contact = info.Name + System.Environment.NewLine + $"Email: {info.Email}" + System.Environment.NewLine + $"Phone: {info.MobilePhone}"; System.Windows.Forms.Clipboard.SetText(contact); } catch { } } ContactViewModel contactTooltipViewModel; public async Task<ContactViewModel> EnsureTooltipViewModel(Contact contact) { var contactInfo = await Channel.GetUserInfo(contact.ID); if(contactTooltipViewModel == null) contactTooltipViewModel = ContactViewModel.Create(contactInfo); else ((ISupportParameter)contactTooltipViewModel).Parameter = contactInfo; return contactTooltipViewModel; } } }
Views/MessagesView.cs(vb)
C#
namespace DXHtmlMessengerSample.Views { using System; using System.Drawing; using System.Windows.Forms; using DevExpress.Utils.Html; using DevExpress.Utils.Html.Internal; using DevExpress.XtraEditors; using DevExpress.XtraEditors.Controls; using DevExpress.XtraGrid.Views.Items; using DXHtmlMessengerSample.ViewModels; public partial class MessagesView : XtraUserControl { public MessagesView() { InitializeComponent(); if(!mvvmContext.IsDesignMode) { InitializeStyles(); InitializeBindings(); InitializeMessageEdit(); } } void InitializeStyles() { Styles.Menu.Apply(messageMenuPopup); Styles.Toolbar.Apply(toolbarPanel); Styles.TypingBox.Apply(typingBox); Styles.NoMessages.Apply(messagesItemsView.EmptyViewHtmlTemplate); messagesItemsView.HtmlImages = DXHtmlMessenger.SvgImages; } void InitializeBindings() { var fluent = mvvmContext.OfType<MessagesViewModel>(); // Bind the messages and contact fluent.SetBinding(gridControl, gc => gc.DataSource, x => x.Messages); fluent.SetBinding(messagesItemsView, mv => mv.FocusedRowObject, x => x.SelectedMessage); fluent.SetBinding(toolbarPanel, tp => tp.DataContext, x => x.Contact); // We need update chat when the ViewModel detect changes fluent.SetTrigger(x => x.Messages, contacts => messagesItemsView.RefreshData()); fluent.SetTrigger(x => x.UpdatedMessageIndices, indices => messagesItemsView.RefreshData(indices)); fluent.SetTrigger(x => x.Contact, contact => messagesItemsView.MoveLast()); // Bind life-cycle events fluent.WithEvent(this, nameof(HandleCreated)) .EventToCommand(x => x.OnCreate); fluent.WithEvent(this, nameof(HandleDestroyed)) .EventToCommand(x => x.OnDestroy); // Bind toolbar elements fluent.BindCommandToElement(toolbarPanel, "btnPhoneCall", x => x.PhoneCall); fluent.BindCommandToElement(toolbarPanel, "btnVideoCall", x => x.VideoCall); fluent.BindCommandToElement(toolbarPanel, "btnContact", x => x.ShowContact); fluent.BindCommandToElement(toolbarPanel, "btnUser", x => x.ShowUser); // Bind typingBox elements fluent.BindCommandToElement(typingBox, "btnSend", x => x.SendMessage); fluent.WithKey(messageEdit, Keys.Control | Keys.Enter) .KeyToCommand(x => x.SendMessage); // Bind editors fluent.SetObjectDataSourceBinding(messageBindingSource, x => x.Update); // Bind context items fluent.BindCommandToElement(messageMenuPopup, "miLike", x => x.LikeMessage); fluent.BindCommandToElement(messageMenuPopup, "miCopy", x => x.CopyMessage); fluent.BindCommandToElement(messageMenuPopup, "miCopyText", x => x.CopyMessageText); fluent.BindCommandToElement(messageMenuPopup, "miDelete", x => x.DeleteMessage); // Bind popup menu showing/hiding messagesItemsView.ElementMouseClick += OnMessagesViewElementMouseClick; messagesItemsView.TopRowPixelChanged += OnMessagesTopRowPixelChanged; messageMenuPopup.Hidden += OnMessageMenuPopupHidden; } CssStyle activeMoreStyle; void OnMessagesViewElementMouseClick(object sender, ItemsViewHtmlElementMouseEventArgs e) { if(e.ElementId == "btnMore") { activeMoreStyle = e.Element.Style; activeMoreRowHandle = e.RowHandle; activeMoreStyle.SetProperty("opacity", "1"); } if(e.ElementId == "btnMore" || e.ElementId == "btnLike") ShowMenu(e); } void ShowMenu(ItemsViewHtmlElementMouseEventArgs e) { var size = ScaleDPI.ScaleSize(new Size(212, 180)); var location = new Point( e.Bounds.X - (size.Width - e.Bounds.Width) / 2, e.Bounds.Y - size.Height + ScaleDPI.ScaleVertical(8)); messageMenuPopup.Show(gridControl, gridControl.RectangleToScreen(new Rectangle(location, size))); } void OnMessagesTopRowPixelChanged(object sender, EventArgs e) { messageMenuPopup.Hide(); } int? activeMoreRowHandle; void OnMessageMenuPopupHidden(object sender, EventArgs e) { activeMoreStyle = null; if(activeMoreRowHandle.HasValue) messagesItemsView.RefreshRow(activeMoreRowHandle.Value); activeMoreRowHandle = null; } void InitializeMessageEdit() { var autoHeightEdit = messageEdit as IAutoHeightControlEx; autoHeightEdit.AutoHeightEnabled = true; autoHeightEdit.HeightChanged += OnMessageHeightChanged; } void OnMessageHeightChanged(object sender, EventArgs e) { var contentSize = typingBox.GetContentSize(); typingBox.Height = contentSize.Height; } void OnQueryItemTemplate(object sender, QueryItemTemplateEventArgs e) { var message = e.Row as DevExpress.DevAV.Chat.Model.Message; if(message == null) return; if(message.IsOwnMessage) Styles.MyMessage.Apply(e.Template); else Styles.Message.Apply(e.Template); var fluent = mvvmContext.OfType<MessagesViewModel>(); fluent.ViewModel.OnMessageRead(message); } void OnCustomizeItem(object sender, CustomizeItemArgs e) { var message = e.Row as DevExpress.DevAV.Chat.Model.Message; if(message == null) return; if(message.IsLiked) { var btnLike = e.Element.FindElementById("btnLike"); var btnMore = e.Element.FindElementById("btnMore"); if(btnLike != null && btnMore != null) { btnLike.Hidden = false; btnMore.Hidden = true; } } if(message.IsFirstMessageOfBlock) return; if(!message.IsOwnMessage) { var avatar = e.Element.FindElementById("avatar"); if(avatar != null) avatar.Style.SetVisibility(CssVisibility.Hidden); } var name = e.Element.FindElementById("name"); if(name != null) name.Hidden = true; if(!message.IsFirstMessageOfReply) { var sent = e.Element.FindElementById("sent"); if(sent != null) sent.Hidden = true; } } sealed class Styles { public static Style Toolbar = new ToolbarStyle(); public static Style Message = new MessageStyle(); public static Style MyMessage = new MyMessageStyle(); public static Style NoMessages = new NoMessagesStyle(); public static Style Menu = new MenuStyle(); public static Style TypingBox = new TypingBoxStyle(); // sealed class ToolbarStyle : Style { } sealed class MessageStyle : Style { } sealed class MyMessageStyle : Style { } sealed class MenuStyle : Style { } sealed class TypingBoxStyle : Style { } sealed class NoMessagesStyle : Style { } } } }
ViewModels/MessagesViewModel.cs(vb)
C#
namespace DXHtmlMessengerSample.ViewModels { using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DevExpress.DevAV.Chat; using DevExpress.DevAV.Chat.Commands; using DevExpress.DevAV.Chat.Events; using DevExpress.DevAV.Chat.Model; using DevExpress.Mvvm; using DevExpress.Mvvm.DataAnnotations; using DevExpress.Mvvm.POCO; public class MessagesViewModel : ChannelViewModel { public MessagesViewModel() : base() { Messages = new Message[0]; Messenger.Default.Register<Contact>(this, OnContact); } public override void OnDestroy() { base.OnDestroy(); Messenger.Default.Unregister<Contact>(this, OnContact); } protected override void OnConnected(IChannel channel) { base.OnConnected(channel); channel.Subscribe(OnMessageEvents); channel.Subscribe(OnContactEvents); } protected override async void OnChannelReady() { await LoadMessages(Channel, Contact); await DispatcherService?.BeginInvoke(UpdateUIOnChannelReady); } async void OnContactEvents(Dictionary<long, ContactEvent> events) { if(Contact != null) { ContactEvent @event; if(events.TryGetValue(Contact.ID, out @event)) { if(@event is UnreadChanged || @event is NewMessages) await LoadMessages(Channel, Contact); } if(events.Count > 0) await DispatcherService?.BeginInvoke(RaiseMessagesChanged); if(@event is NewMessages) await DispatcherService?.BeginInvoke(RaiseContactChanged); } } HashSet<int> updatedMessagesIdices = new HashSet<int>(); async void OnMessageEvents(Dictionary<long, MessageEvent> events) { updatedMessagesIdices.Clear(); MessageEvent @event = null; int index = 0; foreach(Message message in Messages) { if(events.TryGetValue(message.ID, out @event) && updatedMessagesIdices.Add(index)) @event.Apply(message); index++; } if(events.Count > 0) await DispatcherService?.BeginInvoke(RaiseMessagesUpdated); } void UpdateUIOnChannelReady() { this.RaisePropertyChanged(x => x.Messages); UpdateActions(); } void RaiseMessagesChanged() { this.RaisePropertyChanged(x => x.Messages); } void RaiseMessagesUpdated() { this.RaisePropertyChanged(x => x.UpdatedMessageIndices); updatedMessagesIdices.Clear(); } async void OnContact(Contact contact) { await LoadMessages(Channel, contact); await DispatcherService?.BeginInvoke(() => this.Contact = contact); } async Task LoadMessages(IChannel channel, Contact contact) { if(channel != null && contact != null) { var history = await channel.GetHistory(contact); await DispatcherService?.BeginInvoke(() => Messages = history); } } public virtual Contact Contact { get; protected set; } void RaiseContactChanged() { this.RaisePropertyChanged(x => x.Contact); } protected void OnContactChanged() { UpdateActions(); } public bool CanExecuteActions() { return (Channel != null) && (Contact != null); } protected void UpdateActions() { this.RaiseCanExecuteChanged(x => x.SendMessage()); this.RaiseCanExecuteChanged(x => x.PhoneCall()); this.RaiseCanExecuteChanged(x => x.VideoCall()); this.RaiseCanExecuteChanged(x => x.ShowContact()); this.RaiseCanExecuteChanged(x => x.ShowUser()); } public virtual IReadOnlyCollection<Message> Messages { get; protected set; } public IReadOnlyCollection<int> UpdatedMessageIndices { get { return updatedMessagesIdices.ToArray(); } } Message lastMessage; protected void OnMessagesChanged() { lastMessage = Messages.LastOrDefault(); } public virtual string MessageText { get; set; } protected void OnMessageTextChanged() { this.RaiseCanExecuteChanged(x => x.SendMessage()); } public bool CanSendMessage() { return CanExecuteActions() && !string.IsNullOrEmpty(MessageText); } public void SendMessage() { if(Channel != null) Channel.Send(new AddMessage(Contact, MessageText)); MessageText = null; } public void Update() { this.RaiseCanExecuteChanged(x => x.SendMessage()); } [Command(CanExecuteMethodName = nameof(CanExecuteActions))] public async void PhoneCall() { var contactInfo = await Channel.GetUserInfo(Contact.ID); DoCall("Phone Call: " + contactInfo.MobilePhone); } [Command(CanExecuteMethodName = nameof(CanExecuteActions))] public async void VideoCall() { var contactInfo = await Channel.GetUserInfo(Contact.ID); DoCall("Video Call: " + contactInfo.MobilePhone); } void DoCall(string call) { var msgService = this.GetRequiredService<IMessageBoxService>(); msgService.ShowMessage(call); } [Command(CanExecuteMethodName = nameof(CanExecuteActions))] public async void ShowContact() { var contactInfo = await Channel.GetUserInfo(Contact.ID); MessengerViewModel.ShowContactInfo(this, contactInfo); } [Command(CanExecuteMethodName = nameof(CanExecuteActions))] public async void ShowUser() { var userInfo = await Channel.GetUserInfo(Channel.UserName); MessengerViewModel.ShowUserInfo(this, userInfo); } public virtual Message SelectedMessage { get; set; } protected void OnSelectedMessageChanged() { this.RaiseCanExecuteChanged(x => x.DeleteMessage()); this.RaiseCanExecuteChanged(x => x.CopyMessage()); this.RaiseCanExecuteChanged(x => x.CopyMessageText()); this.RaiseCanExecuteChanged(x => x.LikeMessage()); } public bool CanDeleteMessage() { return (SelectedMessage != null) && !SelectedMessage.IsDeleted; } public void DeleteMessage() { if(Channel != null) Channel.Send(new DeleteMessage(SelectedMessage.ID)); } public bool CanCopyMessage() { return (SelectedMessage != null) && !SelectedMessage.IsDeleted; } public void CopyMessage() { try { string message = "[" + SelectedMessage.StatusText + "] " + SelectedMessage.Owner.UserName + System.Environment.NewLine + SelectedMessage.Text; System.Windows.Forms.Clipboard.SetText(message); } catch { } } public bool CanCopyMessageText() { return (SelectedMessage != null) && !SelectedMessage.IsDeleted; } public void CopyMessageText() { try { System.Windows.Forms.Clipboard.SetText(SelectedMessage.Text); } catch { } } public bool CanLikeMessage() { return (SelectedMessage != null) && !SelectedMessage.IsLiked; } public void LikeMessage() { if(Channel != null) Channel.Send(new LikeMessage(SelectedMessage.ID)); } [Command(false)] public void OnMessageRead(Message message) { if(lastMessage != null && message == lastMessage) { lastMessage = null; if(Channel != null) Channel.Send(new ReadMessages(Contact)); } } } }
Docs/ApplicationPartsDesign.md
Code
# Application UI Design ### Search Panel ![search panel](./Images/dxhtmlmessenger-searchbox.png) The Search Panel is implemented by the [HtmlContentControl](https://docs.devexpress.com/WindowsForms/DevExpress.XtraEditors.HtmlContentControl). It uses HTML markup and CSS from the following files to render an icon and input box ([SearchControl](https://docs.devexpress.com/WindowsForms/DevExpress.XtraEditors.SearchControl)). - [Assets/Html/searchPanel.html](../Assets/Html/searchPanel.html) - [Assets/CSS/searchPanel.css](../Assets/CSS/searchPanel.css) The HTML code from the [searchPanel.html](../Assets/Html/searchPanel.html) file is shown below: ```html <div class='panel'> <img src='Search' class='searchButton'/> <input name="searchControl" class="searchInput"> </div>

Input Box

Follow the steps below to add an input box to the HtmlContentControl:

  • Define the input tag in HTML markup.
  • Place any text editor (the SearchControl in this example) onto the HtmlContentControl. Set the text editor's name ("searchControl" in this example).
  • Set the name property of the input tag to the name of the created text editor ("searchControl").

The SearchControl automatically filters its client control (SeachControl.Client) according to the entered text. In this example, the SearchControl's client is the GridControl that displays contacts.

Typing Box

typing box

The Typing Box is implemented by the HtmlContentControl. It uses HTML markup and CSS from the following files to display an input box (MemoEdit) and a Send button.

The HTML code from the typingbox.html file is shown below:

HTML
<div class='typingBox'> <div class='container'> <input name="messageEdit" class="message" /> <div class='separator-Container'></div> <img id="btnSend" src='Send' class='button' /> </div> </div>

Input Box

You can add an input box to the HtmlContentControl the same way as the Search Panel.

Buttons

Follow the steps below to render a button:

  • Define an HTML element that renders the button (the img tag in this example). Specify the element's class (button in this example).
  • In the corresponding CSS file, define the button class to specify the element's display properties.
  • You should also define the hover state for the button class to highlight the element when it's hovered over.

The following snippet from the typingbox.css file demonstrates this approach:

CSS
.button { width: 23px; height: 23px; padding: 8px; opacity:0.5; } .button:hover { border-radius: 4px; background-color: @Control; }

You can handle the HtmlContentControl.ElementMouseClick event to perform actions when an element (button) is clicked. The current application uses Fluent API to define the button's action on a click.

C#
fluent.BindCommandToElement(typingBox, "btnSend", x => x.SendMessage); //... public void SendMessage() { if(Channel != null) Channel.Send(new AddMessage(Contact, MessageText)); MessageText = null; }
Visual Basic
fluent.BindCommandToElement(typingBox, "btnSend", Sub(x) x.SendMessage()) '... Public Sub SendMessage() If Channel IsNot Nothing Then Channel.Send(New AddMessage(Contact, MessageText)) End If MessageText = Nothing End Sub

Toolbar

typing box

The Toolbar is implemented by the HtmlContentControl. It uses HTML markup and CSS from the following files to display a contact name, and render buttons.

The HTML code from the toolbar.html file is shown below:

HTML
<div class='toolBar'> <div class='contactName'>${UserName}</div> <div class='buttonPanel'> <img id="btnPhoneCall" src='PhoneCall' class='button' title="Phone Call" /> <img id="btnVideoCall" src='VideoCall' class='button' title="Video Call"/> <img id="btnContact" src='Contact' class='button' title="Show Contact Info"/> <div class='separator-Container'> <div class='separator'></div> </div> <img id="btnUser" src='User' class='button' title="Show User Info"/> </div> </div>

Data Binding - Display Field Values

The example uses the HtmlContentControl.DataContext property to bind the HtmlContentControl to the MessagesViewModel.Contact business object—this data context supplies data to the control.

In the HTML code above, the '$' character at the beginning of the "${UserName}" string specifies that the string that follows is an interpolated string—an expression that the control needs to evaluate. The "{FieldName}" form is the syntax for data binding and is used to insert a value of the specified field in the output. The "${UserName}" text inserts a value of the UserName field from the data context.

An interpolated string can contain static text, data binding to multiple fields, and field value formatting (see string interpolation for more information). The following example adds the 'Welcome' string before the user name:

HTML
<h1>$Welcome {UserName}!</h1>

Buttons

Buttons are added to the HtmlContentControl in the same way as described in the Typing Box section.

Contacts

contact list

The contact list is implemented by the GridControl's TileView. Each record (contact) in a TileView is a tile—a non-editable box that arranges fields based on a specific template. The TileView allows you to set the tile template in two ways:

  • In HTML format (as demonstrated in the current application).
  • Using the common Table Layout concept (see Tile View Template).

In this application, HTML markup and CSS from the following files are used to render each tile:

The HTML code from the contact.html file is shown below:

Data Binding - Display Field Values

The "$FieldName" syntax is used to display values of corresponding fields from a bound data source (GridControl.DataSource). The same data binding approach is demonstrated in the Toolbar section above.

Customize Individual Elements

The application handles the TileView.ItemCustomize event to dynamically control the visibility of tile elements.

C#
void OnContactItemTemplateCustomize(object sender, TileViewItemCustomizeEventArgs e) { var contact = contactsTileView.GetRow(e.RowHandle) as Contact; if(contact != null) { var statusBadge = e.HtmlElement.FindElementById("statusBadge"); if(statusBadge != null && !contact.IsInactive) statusBadge.Style.SetBackgroundColor("@Green"); if(!contact.HasUnreadMessages) { var unreadBadge = e.HtmlElement.FindElementById("unreadBadge"); if(unreadBadge != null) unreadBadge.Hidden = true; } } }
Visual Basic
Sub OnContactItemTemplateCustomize(ByVal sender As Object, ByVal e As TileViewItemCustomizeEventArgs) Handles contactsTileView.ItemCustomize Dim contact = TryCast(contactsTileView.GetRow(e.RowHandle), Contact) If contact IsNot Nothing Then Dim statusBadge = e.HtmlElement.FindElementById("statusBadge") If statusBadge IsNot Nothing AndAlso (Not contact.IsInactive) Then statusBadge.Style.SetBackgroundColor("@Green") End If If Not contact.HasUnreadMessages Then Dim unreadBadge = e.HtmlElement.FindElementById("unreadBadge") If unreadBadge IsNot Nothing Then unreadBadge.Hidden = True End If End If End If End Sub

Messages

message list

The message list is implemented by the GridControl's ItemsView — a View that renders each item (message) based on a specific HTML template.
Items are arranged vertically in the current application.

You can find the HTML templates and CSS styles applied to items in the following files:

The HTML code from the message.html file is shown below:

HTML
<div class='message'> <img id="avatar" class="avatar" src='${Owner.Avatar}' /> <div class='container'> <div id="name" class='name'>${Owner.UserName}</div> <div id="sent" class='sent'>${StatusText}</div> <div class="textAndMore"> <div class='text'>${Text}</div> <img id="btnMore" class='more' src='Menu' /> <img id="btnLike" class='like' src='Like' hidden /> </div> </div> </div>

Data Binding - Display Field Values

In the application, the GridControl's ItemsView is bound to the MessagesViewModel.Messages collection.
An element's "${Owner.Avatar}" syntax in HTML markup is used to bind this element to the Owner.Avatar field on the underlying data store. This data binding technique is described in the Toolbar section above.

Use Different Tempates for Different Items

The ItemsView.QueryItemTemplate event is handled to assign different templates to messages of the current user and messages of other users.

C#
void OnQueryItemTemplate(object sender, DevExpress.XtraGrid.Views.Items.QueryItemTemplateEventArgs e) { var message = e.Row as DevExpress.DevAV.Chat.Model.Message; if(message == null) return; if(message.IsOwnMessage) Styles.MyMessage.Apply(e.Template); else Styles.Message.Apply(e.Template); var fluent = mvvmContext.OfType<MessagesViewModel>(); fluent.ViewModel.OnMessageRead(message); }
Visual Basic
Sub OnQueryItemTemplate(ByVal sender As Object, ByVal e As DevExpress.XtraGrid.Views.Items.QueryItemTemplateEventArgs) Handles messagesItemsView.QueryItemTemplate Dim message = TryCast(e.Row, DevExpress.DevAV.Chat.Model.Message) If message Is Nothing Then Return End If If message.IsOwnMessage Then Styles.MyMessage.Apply(e.Template) Else Styles.Message.Apply(e.Template) End If Dim fluent = mvvmContext.OfType(Of MessagesViewModel)() fluent.ViewModel.OnMessageRead(message) End Sub

Customize Individual Elements

The application handles the ItemsView.CustomizeItem event to dynamically customize individual elements of items.

C#
void OnCustomizeItem(object sender, CustomizeItemArgs e) { var message = e.Row as DevExpress.DevAV.Chat.Model.Message; if(message == null) return; if(message.IsLiked) { var btnLike = e.Element.FindElementById("btnLike"); var btnMore = e.Element.FindElementById("btnMore"); if(btnLike != null && btnMore != null) { btnLike.Hidden = false; btnMore.Hidden = true; } } if(message.IsFirstMessageOfBlock) return; if(!message.IsOwnMessage) { var avatar = e.Element.FindElementById("avatar"); if(avatar != null) avatar.Style.SetVisibility(CssVisibility.Hidden); } var name = e.Element.FindElementById("name"); if(name != null) name.Hidden = true; if(!message.IsFirstMessageOfReply) { var sent = e.Element.FindElementById("sent"); if(sent != null) sent.Hidden = true; } }
Visual Basic
Sub OnCustomizeItem(ByVal sender As Object, ByVal e As CustomizeItemArgs) Handles messagesItemsView.CustomizeItem Dim message = TryCast(e.Row, DevExpress.DevAV.Chat.Model.Message) If message Is Nothing OrElse message.IsFirstMessageOfBlock Then Return End If If message.IsLiked Then Dim btnLike = e.Element.FindElementById("btnLike") Dim btnMore = e.Element.FindElementById("btnMore") If btnLike IsNot Nothing And btnMore IsNot Nothing Then btnLike.Hidden = False btnMore.Hidden = True End If End If If message.IsFirstMessageOfBlock Then Return End If If Not message.IsOwnMessage Then Dim avatar = e.Element.FindElementById("avatar") If avatar IsNot Nothing Then avatar.Style.SetVisibility(CssVisibility.Hidden) End If End If Dim name = e.Element.FindElementById("name") If name IsNot Nothing Then name.Hidden = True End If If Not message.IsFirstMessageOfReply Then Dim sent = e.Element.FindElementById("sent") If sent IsNot Nothing Then sent.Hidden = True End If End If End Sub

The HtmlContentPopup control is a pop-up control that renders its contents from HTML markup and CSS styles. The application uses HtmlContentPopup controls to create the following pop-up windows:

  • Pop-up windows that show information about the current user and the contact that the user chats with.
  • A log off window.
  • A pop-up menu that is displayed when you click the 'Thumb Up' icon for a message.

popup menu

HTML markup and CSS styles from the following files are used to render this pop-up menu:

The HTML code from the menu.html file is shown below:

HTML
<div class="container"> <div class="shadow"> <div class="menu"> <div id="miCopy" class="menuItem">Copy Message</div> <div id="miCopyText" class="menuItem">Copy Text</div> <div class="separator"></div> <div id="miLike" class="menuItem"> <img class='like' src='Like' /> <span>Like Message</span> </div> </div> </div> <img class='beak-bottom' src='Beak.Bottom' /> </div>

The HtmlContentPopup.Show method is used to display the pop-up window:

C#
messageMenuPopup.Show(gridControl, menuScreenBounds);

The HtmlContentPopup.Hide method allows you to close the pop-up window.

You can handle the HtmlContentPopup.ElementMouseClick event to perform actions when a user clicks on an element within the HtmlContentPopup control. The current application uses Fluent API to bind actions to the pop-up control's elements:

C#
fluent.BindCommandToElement(messageMenuPopup, "miLike", x => x.LikeMessage); fluent.BindCommandToElement(messageMenuPopup, "miCopy", x => x.CopyMessage); fluent.BindCommandToElement(messageMenuPopup, "miCopyText", x => x.CopyMessageText); fluent.BindCommandToElement(messageMenuPopup, "miDelete", x => x.DeleteMessage);
Visual Basic
fluent.BindCommandToElement(messageMenuPopup, "miLike", Sub(x) x.LikeMessage()) fluent.BindCommandToElement(messageMenuPopup, "miCopy", Sub(x) x.CopyMessage()) fluent.BindCommandToElement(messageMenuPopup, "miCopyText", Sub(x) x.CopyMessageText()) fluent.BindCommandToElement(messageMenuPopup, "miDelete", Sub(x) x.DeleteMessage())

See Also

Code
Docs/HowItWorksWithData.md
Code
# Work with data DXHtmlMessenger has three separate layers — a Data Layer, Model, and User Interface. The Data Layer uses interfaces to communicate with the Model layer. This allows you to implement your own Data Layer for DXHtmlMessenger to interact with your servers while leaving the Model and UI layers intact. The following diagram shows the communication between layers: ![server-client](./Images/dxhtmlmessenger-server-client.png) ### Server and Channels The application defines the _IMessageServer_ interface. It contains the _Connect_ method that creates a channel—an object that transfers data, server events, and client commands. ```cs public interface IMessageServer { Task<IChannel> Create(string userName); }
Visual Basic
Public Interface IMessageServer Function Create(ByVal userName As String) As Task(Of IChannel) End Interface

A channel (an IChannel object) returned by the Connect method is associated with a specific user.

C#
public interface IChannel : IDisposable { string UserName { get; } }
Visual Basic
Public Interface IChannel Inherits IDisposable ReadOnly Property UserName() As String End Interface

The following code uses the Connect method to asynchronously create a channel. The returned object contains methods to work with data.

C#
IChannel channel = await server.Create("Jonh Heart");
Visual Basic
Dim channel As IChannel = Await server.Create("Jonh Heart")

When the channel is no longer needed, you can dispose of it:

C#
channel.Dispose();
Visual Basic
channel.Dispose()

The current implementation of the data layer does not require a network connection. A local in-memory server obtains sample data from the DevAV database. All server events are emulated on your local machine.

The in-memory server is registered at startup, as follows.

C#
static Program() { // Register global dependencies DevExpress.Mvvm.ServiceContainer.Default.RegisterService(new DevExpress.DevAV.Chat.DevAVEmpployeesInMemoryServer(createDB)); }
Visual Basic
Shared Sub New() ' Register global dependencies DevExpress.Mvvm.ServiceContainer.Default.RegisterService(New DevExpress.DevAV.Chat.DevAVEmpployeesInMemoryServer(createDB)) End Sub

If you create your own implementation of the data layer, register it here.

Channel and Data

A channel provides means for two-way communication:

  • You can use Subscribe methods to listen to the channel's events.
  • You can use the Send method to send commands to the channel.

The channel contains three types of Send and Subscribe methods: general, contact-aware, and message-aware.

C#
public interface IChannel : IDisposable { // Common operations. void Subscribe(Action<ChannelEvent> onEvent); void Send(ChannelCommand command); // Contact-aware operations. void Subscribe(Action<Dictionary<long, ContactEvent>> onEvents); void Send(ContactCommand command); // Message-aware operations. void Subscribe(Action<Dictionary<long, MessageEvent>> onEvents); void Send(MessageCommand command); ... }
Visual Basic
Public Interface IChannel Inherits IDisposable ' Common operations. Sub Subscribe(ByVal onEvent As Action(Of ChannelEvent)) Sub Send(ByVal command As ChannelCommand) ' Contact-aware operations. Sub Subscribe(ByVal onEvents As Action(Of Dictionary(Of Long, ContactEvent))) Sub Send(ByVal command As ContactCommand) ' Message-aware operations. Sub Subscribe(ByVal onEvents As Action(Of Dictionary(Of Long, MessageEvent))) Sub Send(ByVal command As MessageCommand) '... End Interface

A channel also contains methods that asynchronously obtain data from the server:

C#
public interface IChannel : IDisposable { // Contacts Task<UserInfo> GetUserInfo(string userName); Task<UserInfo> GetUserInfo(long id); Task<IReadOnlyCollection<Contact>> GetContacts(); // Messages Task<IReadOnlyCollection<Message>> GetHistory(Contact contact); }
Visual Basic
Public Interface IChannel Inherits IDisposable ' Contacts Function GetUserInfo(ByVal userName As String) As Task(Of UserInfo) Function GetUserInfo(ByVal id As Long) As Task(Of UserInfo) Function GetContacts() As Task(Of IReadOnlyCollection(Of Contact)) ' Messages Function GetHistory(ByVal contact As Contact) As Task(Of IReadOnlyCollection(Of Message)) End Interface

Data is retrieved from the server when the channel is ready, and then it is displayed within the UI layer.

C#
protected override async void OnChannelReady() { var channelContacts = await Channel.GetContacts(); await DispatcherService?.BeginInvoke(() => Contacts = channelContacts); }
Visual Basic
Protected Overrides Async Sub OnChannelReady() Dim channelContacts = Await Channel.GetContacts() Await DispatcherService?.BeginInvoke(Sub() Contacts = channelContacts) End Sub

You can request data again when a specific server-side event is received.

C#
channel.Subscribe(OnMessageEvents); //... async void OnMessageEvents(Dictionary<long, MessageEvent> events) { updatedMessagesIdices.Clear(); MessageEvent @event = null; int index = 0; foreach(Message message in Messages) { if(events.TryGetValue(message.ID, out @event) && updatedMessagesIdices.Add(index)) @event.Apply(message); index++; } if(events.Count > 0) await DispatcherService?.BeginInvoke(RaiseMessagesUpdated); }
Visual Basic
channel.Subscribe(AddressOf OnMessageEvents) '... Async Sub OnMessageEvents(ByVal events As Dictionary(Of Long, MessageEvent)) updatedMessagesIdicesCore.Clear() Dim [event] As MessageEvent = Nothing Dim index As Integer = 0 For Each message As Message In Messages If events.TryGetValue(message.ID, [event]) And updatedMessagesIdicesCore.Add(index) Then [event].Apply(message) End If index = index + 1 Next message If events.Count > 0 Then Await DispatcherService?.BeginInvoke(AddressOf RaiseMessagesUpdated) End If End Sub

Events

The first group of events is related to authentication:

C#
// The server sends this event when a user access token is required for authentication. public class CredentialsRequiredEvent : ChannelEvent { /*methods*/ } // The server sends this event when user authentication is complete, and the channel is ready for data requests. public class ChannelReadyEvent : ChannelEvent { /* this event has no data or methods */ }
Visual Basic
' The server sends this event when a user access token is required for authentication. Public Class CredentialsRequiredEvent Inherits ChannelEvent 'methods End Class ' The server sends this event when user authentication is complete, and the channel is ready for data requests. Public Class ChannelReadyEvent Inherits ChannelEvent ' this event has no data or methods End Class

When a client receives the CredentialsRequiredEvent event, it should call the SetAccessTokenQuery method. The method specifies a task that asynchronously provides an access-token for a specific user:

C#
void OnChannelEvent(ChannelEvent @event) { var credentialsRequired = @event as CredentialsRequiredEvent; if(credentialsRequired != null) { if(0 == authCounter++) { // provide the access token from local cache without interaction var cacheQuery = QueryAccessTokenFromLocalAuthCache(@event.UserName, credentialsRequired.Salt); credentialsRequired.SetAccessTokenQuery(cacheQuery); } else { // or query access-token asynchronously for the specific user var userQuery = QueryAccessTokenFromUser(@event.UserName, credentialsRequired.Salt); credentialsRequired.SetAccessTokenQuery(userQuery); } } }
Visual Basic
Sub OnChannelEvent(ByVal [event] As ChannelEvent) Dim credentialsRequired = TryCast([event], CredentialsRequiredEvent) If credentialsRequired IsNot Nothing Then AuthCounter += 1 If AuthCounter = 1 Then ' provide the access token from local cache without interaction Dim cacheQuery = QueryAccessTokenFromLocalAuthCache([event].UserName, credentialsRequired.Salt) credentialsRequired.SetAccessTokenQuery(cacheQuery) Else ' or query access-token asynchronously for the specific user Dim userQuery = QueryAccessTokenFromUser([event].UserName, credentialsRequired.Salt) credentialsRequired.SetAccessTokenQuery(userQuery) End If End If End Sub

This task can either obtain the access token from the client-side cache, or show a Sign-In dialog to return credentials from the user.

C#
Task<string> QueryAccessTokenFromUser(string userName, string salt) { var accessTokenQueryCompletionSource = new TaskCompletionSource<string>(); dispatcher.BeginInvoke(() => { var signInViewModel = SignInViewModel.Create(userName, salt); signInViewModel.ShowDialog(); if(!string.IsNullOrEmpty(signInViewModel.AccessToken)) accessTokenQueryCompletionSource.SetResult(signInViewModel.AccessToken); else accessTokenQueryCompletionSource.SetCanceled(); }); return accessTokenQueryCompletionSource.Task; }
Visual Basic
Function QueryAccessTokenFromUser(ByVal userName As String, ByVal salt As String) As Task(Of String) Dim accessTokenQueryCompletionSource = New TaskCompletionSource(Of String)() dispatcher.BeginInvoke(Sub() Dim signInViewModel = ViewModels.SignInViewModel.Create(userName, salt) signInViewModel.ShowDialog() If Not String.IsNullOrEmpty(signInViewModel.AccessToken) Then accessTokenQueryCompletionSource.SetResult(signInViewModel.AccessToken) Else accessTokenQueryCompletionSource.SetCanceled() End If End Sub) Return accessTokenQueryCompletionSource.Task End Function

The second group of events are data-aware. The server sends these events when it encounters changes on the server:

C#
public class StatusChanged : ContactEvent { /* data fields */ }
Visual Basic
Public Class StatusChanged Inherits ContactEvent ' data fields End Class

When the client receives these events, it applies the changes to the corresponding UI controls.

C#
async void OnContactEvents(Dictionary<long, ContactEvent> events) { ContactEvent @event; foreach(Contact contact in Contacts) { if(events.TryGetValue(contact.ID, out @event)) @event.Apply(contact); } if(events.Count > 0) await DispatcherService?.BeginInvoke(RaiseContactsChanged); }
Visual Basic
Async Sub OnContactEvents(ByVal events As Dictionary(Of Long, ContactEvent)) Dim [event] As ContactEvent = Nothing For Each contact As Contact In Contacts If events.TryGetValue(contact.ID, [event]) Then [event].Apply(contact) End If Next contact If events.Count > 0 Then Await DispatcherService?.BeginInvoke(AddressOf RaiseContactsChanged) End If End Sub

Commands

Commands are sent from clients to the server. For example, a command can initiate a log off procedure.

C#
public void LogOff() { if(channel != null) channel.Send(new LogOff(channel)); }
Visual Basic
Public Sub LogOff() If channel IsNot Nothing Then channel.Send(New LogOff(channel)) End If End Sub

Another example of a command is sending a new chat message to the server:

C#
public void SendMessage() { if(Channel != null) Channel.Send(new AddMessage(Contact, MessageText)); MessageText = null; }
Visual Basic
Public Sub SendMessage() If Channel IsNot Nothing Then Channel.Send(New AddMessage(Contact, MessageText)) End If MessageText = Nothing End Sub

The server should respond to these commands (for instance, update data on the server and send notification events to client channels).

See Also

Code

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.