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.
Files to Look At
Model
Data Layer
- IMessageServer.cs (IMessageServer.vb)
- ChannelEvent.cs (ChannelEvent.vb)
- ContactEvents.cs (ContactEvents.vb)
- MessageEvents.cs (MessageEvents.vb)
- ChannelCommands.cs (ChannelCommands.vb)
- ContactCommands.cs (ContactCommands.vb)
- MessageCommands.cs (MessageCommands.vb)
User Interface
- Messenger.cs & MessengerViewModel.cs (Messenger.vb & MessengerViewModel.vb)
- ContactsView.cs & ContactsViewModel.cs (ContactsView.vb & ContactsViewModel.vb)
- MessagesView.cs & MessagesViewModel.cs (MessagesView.vb & MessagesViewModel.vb)
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.
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).
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
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;
}
}
}
Visual BasicImports 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
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;
}
}
}
Visual BasicImports 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
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);
}
}
Visual BasicImports 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
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) {
}
}
}
Visual BasicImports 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
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 */
}
}
}
Visual BasicImports 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
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();
}
}
}
Visual BasicImports 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
C#namespace DevExpress.DevAV.Chat.Commands {
public abstract class ChannelCommand {
public IChannel Channel {
get;
}
}
//
public class LogOff : ChannelCommand {
public LogOff(IChannel channel)
: base(channel) {
}
}
}
Visual BasicNamespace 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
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) {
}
}
}
Visual BasicImports 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
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) {
}
}
}
Visual BasicNamespace 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
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 { }
}
}
}
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);
}
}
}
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 { }
}
}
}
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;
}
}
}
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 { }
}
}
}
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));
}
}
}
}
Code# Application UI Design
### Search Panel

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 theinput
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
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 thebutton
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 Basicfluent.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
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
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 BasicSub 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
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:
- Assets/Html/message.html
- Assets/CSS/message.css
- Assets/Html/mymessage.html (applied to messages written by the current user).
- Assets/CSS/mymessage.css (applied to messages written by the current user).
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 BasicSub 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 BasicSub 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
Popup Window
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.
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 Basicfluent.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
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 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 BasicPublic 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 BasicPublic 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 BasicDim channel As IChannel = Await server.Create("Jonh Heart")
When the channel is no longer needed, you can dispose of it:
C#channel.Dispose();
Visual Basicchannel.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 BasicShared 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 BasicPublic 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 BasicPublic 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 BasicProtected 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 Basicchannel.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 BasicSub 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 BasicFunction 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 BasicPublic 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 BasicAsync 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 BasicPublic 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 BasicPublic 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