Service Side
I try to make the Sub/Pub implemenation as generic as possible to be able to re-use it in others service.Generic Implementation
ISubscribeService
Firstly, i begin to define a ISubscribeService Interface wich will allow me after to have an implementation Class to make subscription on the service for the notifications.
This is the interface definition:
using System.ServiceModel;
[ServiceContract]
public interface ISubscribeService
{
/// <summary>
/// This method should be executed to subscribe to notifications from server.
/// </summary>
[OperationContract(IsOneWay = true)]
void SubscribeToNotifications(string clientId);
/// <summary>
/// This method should be executed to unsubscribe to notifications from server.
/// </summary>
[OperationContract(IsOneWay = true)]
void UnsubscribeToNotifications(string clientId);
}
ServiceBase<T>
This is the generic class used to write the Sub/Pub mechanism for the service.
using System;
using System.ServiceModel;
using System.Timers;
public class ServiceBase<T>
{
#region variables
#endregion
#region Constructors
protected ServiceBase()
{
SyncRoot = new Object();
Timer = new Timer();
}
#endregion
#region Properties
protected Object SyncRoot { get; private set; }
protected Timer Timer { get; private set; }
#endregion
#region methods
/// <summary>
/// This method should be executed to subscribe to notifications from server.
/// </summary>
protected void SubscribeToNotifications(string clientId)
{
var channel = OperationContext.Current.GetCallbackChannel<T>();
//Any message from a client we haven't seen before causes the new client to be added to our list
lock (SyncRoot)
{
if (SessionsController<T>.IsClientConnected(clientId))
return;
SessionsController<T>.AddCallbackChannel(clientId, channel);
}
}
/// <summary>
/// This method should be executed to unsubscribe to notifications from server.
/// </summary>
protected void UnsubscribeToNotifications(string clientId)
{
lock (SyncRoot)
{
SessionsController<T>.DeleteClient(clientId);
}
}
#endregion
}
SessionsController<T>
A geneic Class used to handle and manage the clients sessions for the service.
using System;
using System.Collections.Generic;
using System.Linq;
public static class SessionsController<T>
{
#region Variables
#endregion
#region Constructors
/// <summary>
/// Class constructor.
/// </summary>
static SessionsController()
{
Clients = new Dictionary<string, T>();
}
#endregion
#region Properties
public static Dictionary<string, T> Clients { get; private set; }
/// <summary>
/// Gets the total callback channels.
/// </summary>
public static int TotalCallbackChannels { get { return GetCallbackChannels().Count(); } }
/// <summary>
/// Returns all available callback channels
/// </summary>
/// <returns>Callback channels</returns>
public static IEnumerable<T> AllCallbackChannels
{
get { return Clients.Select(c => c.Value); }
}
#endregion
#region Methods
/// <summary>
/// Stores given client callback channel
/// </summary>
/// <param name="clientId">Id of client's session</param>
/// <param name="callbackChannel">Callback channel for given client</param>
public static void AddCallbackChannel(string clientId, T callbackChannel)
{
if (Clients != null && Clients.Count(c => c.Key == clientId) != 0)
return;
var syncRoot = new Object();
lock (syncRoot)
{
if (Clients != null && Clients.All(c => c.Key != clientId))
{
Clients.Add(clientId, callbackChannel);
}
}
}
/// <summary>
/// Checks whether given client is connected.
/// </summary>s
/// <param name="clientId">Id of client's session</param>
/// <returns>Whether client is connected</returns>
public static bool IsClientConnected(string clientId)
{
return Clients != null && Clients.Any(c => c.Key == clientId);
}
/// <summary>
/// Returns all available callback channels except sender
/// </summary>
/// <returns>Callback channels</returns>
public static IEnumerable<T> GetCallbackChannels(string sessionId)
{
var filteredChannels = Clients.Where(channel => channel.Key != sessionId);
return filteredChannels.Select(c => c.Value);
}
/// <summary>
/// Returns callback channel for given client
/// </summary>
/// <param name="clientId">Id of client's session</param>
/// <returns>Callback channel for given client</returns>
public static T GetCallbackChannel(string clientId)
{
return Clients != null ? Clients.Single(c => c.Key == clientId).Value : default(T);
}
/// <summary>
/// Returns all available callback channels
/// </summary>
/// <returns>Callback channels</returns>
public static IEnumerable<T> GetCallbackChannels()
{
return Clients.Select(c => c.Value);
}
/// <summary>
/// Deletes callback channel for given client
/// </summary>
/// <param name="clientId">Id of client's session</param>
public static void DeleteClient(string clientId)
{
if (Clients == null || Clients.All(c => c.Key != clientId))
return;
var syncRoot = new Object();
lock (syncRoot)
{
if (Clients.Any(c => c.Key == clientId))
{
Clients.Remove(clientId);
}
}
}
#endregion
}
Use Case in an example service
In my case, i used a service named StockService[ServiceContract(CallbackContract = typeof(IClientCallBack))]
public interface IStockService : ISubscribeService //The service interface inherit the interface defined early.
{
//You can define your Operations Contract
}
i passed IClientCallBack as value for the CallbackContract property for the ServiceContract Attribute defined below:
public interface IClientCallBack
{
[OperationContract(IsOneWay = true)]
void SendProductsList(string[] list);
}
Once all those done, i describe the StockService implementation:
public class StockService : ServiceBase<IClientCallBack>, IStockService
{
#region Notification
/// <summary>
/// This method should be executed to subscribe to notifications from server.
/// </summary>
void ISubscribeService.SubscribeToNotifications(string clientId)
{
SubscribeToNotifications(clientId);
}
/// <summary>
/// This method should be executed to unsubscribe to notifications from server.
/// </summary>
void ISubscribeService.UnsubscribeToNotifications(string clientId)
{
UnsubscribeToNotifications(clientId);
}
#endregion
public StockService()
{
Timer.Elapsed += Process;
Timer.Interval = 2000;
Timer.Enabled = true;
Timer.Start();
}
private void Process(object sender, EventArgs e)
{
var list = new List<string>();
for(var i = 0; i <100; i++)
{
list.Add("Item" + i);
}
if (SessionsController<IClientCallBack>.TotalCallbackChannels <= 0)
return;
lock(SyncRoot)
{
var allChannels = SessionsController<IClientCallBack>.GetCallbackChannels();
allChannels.ToList().ForEach(c => c.SendProductsList(list.ToArray()));
}
}
}
This is the ServiceModel configuration used in my web.config:
<system.serviceModel>
<services>
<service behaviorConfiguration="StockServiceTypeBehavior" name="Stock.StockService">
<endpoint address="" binding="wsDualHttpBinding" contract="Stock.IStockService"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="StockServiceTypeBehavior">
<!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment -->
<serviceMetadata httpGetEnabled="true"/>
<!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>
After that, all it's done for the service. let's go to check out for the Client.
Client Side
I begin to define a CallbackController
internal class CallbackController : IStockServiceCallback
{
public void SendProductsList(string[] list)
{
MessageMediator.SendMessage(list, "Tag"); //You have to use a Message Mediator implementation
}
I used a Catel ViewModel in my client:
/// <summary>
/// MainWindow view model.
/// </summary>
[CallbackBehavior(UseSynchronizationContext = false)]
public class MainWindowViewModel : ViewModelBase
{
#region Variables
private StockServiceClient Proxy { get; set; }
private string ClientId { get; set; }
private StockServiceClient Client { get; set; }
#endregion
#region Constructor & destructor
/// <summary>
/// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
/// </summary>
public MainWindowViewModel()
{
MessageMediator .Register<string[]>(this,
(x) =>
{ Liste = new ObservableCollection<string>(x); },
"Tag");
ClientId = Guid.NewGuid().ToString();
Client = new StockServiceClient(new InstanceContext(new CallbackController()));
Client.SubscribeToNotifications(ClientId);
}
#endregion
#region Properties
/// <summary>
/// Gets the title of the view model.
/// </summary>
/// <value>The title.</value>
public override string Title { get { return "View model title"; } }
/// <summary>
/// Gets or sets the property value.
/// </summary>
public ObservableCollection<string> Liste
{
get { return GetValue<ObservableCollection<string>>(ListeProperty); }
set { SetValue(ListeProperty, value); }
}
/// <summary>
/// Register the Liste property so it is known in the class.
/// </summary>
public static readonly PropertyData ListeProperty = RegisterProperty("Liste", typeof(ObservableCollection<string>));
#endregion
#region Commands
// TODO: Register commands with the vmcommand or vmcommandwithcanexecute codesnippets
#endregion
#region Methods
protected override void Close()
{
Client.UnsubscribeToNotifications(ClientId);
}
#endregion
}
This is the ServiceModel configuration in my app.config:
<system.serviceModel>
<bindings>
<wsDualHttpBinding>
<binding name="WSDualHttpBinding_IStockService" />
</wsDualHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:807/StockService.svc" binding="wsDualHttpBinding"
bindingConfiguration="WSDualHttpBinding_IStockService" contract="Service.IStockService"
name="WSDualHttpBinding_IStockService">
<identity>
<userPrincipalName value="Host\Rajiv" />
</identity>
</endpoint>
</client>
</system.serviceModel>
That's all.
I just hope that help someone (really sorry for the presentation, i will try to improve :)).
Waiting yours feedback.