I have done what I can only explain to be exhaustive searching for an answer to my problem and I have found numerous examples that I thought would point me in the right direction and seem to always fall short. I am sure that there is a simple solution to my problem but it is escaping me.
What I am trying to accomplish:
I have a server that is hosting a WCF Service (duplex binding) within a WPF application. The server is also hosting a WPF Browser application through IIS. The intent is a client will download and run the browser application. The browser application should connect to the service on the server. The client will submit a request to the service to connect. The service remembers the callback to each client that connects. Then, whenever information within the application on the server changes it passes that information to the clients through the stored callbacks.
The issue that I am having:
When I store the callbacks it appears that each callback is identical. Therefore, if I connect from two clients when the service passes updates to the clients it sends the update to the last client to connect twice. If I then connect from a third client, the service sends the update to that client three times. This continues on for however many clients I connect to the service with from however many different machines I run the browser (client) application from.
SERVICE SIDE
.xaml code
<Window x:Class="WCF_Service.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Loaded="Window_Loaded">
<Button Content="Call Clients" Height="23" HorizontalAlignment="Left" Margin="12,0,0,12" Name="btnCall" VerticalAlignment="Bottom" Width="75" Click="btnCall_Click" />
<TextBox Margin="12,12,12,41" Name="txtActivity" TextWrapping="Wrap" />
</Grid>
</Window>
Service Source Code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ServiceModel;
using System.ServiceModel.Description;
namespace WCF_Service
{
[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IMyServiceCallback))]
public interface IMyServiceContract
{
[OperationContract(IsOneWay = true)]
void Connect(string user);
}
public interface IMyServiceCallback
{
[OperationContract(IsOneWay = true)]
void MyCallback();
}
public class MyClient
{
public MyClient()
{
_clientCallback = null;
_username = "";
}
private static IMyServiceCallback _clientCallback;
public IMyServiceCallback ClientCallback
{
get { return _clientCallback; }
set { _clientCallback = value; }
}
private string _username;
public string Username
{
get { return _username; }
set { _username = value; }
}
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, InstanceContextMode = InstanceContextMode.Single)]
public class MyService : IMyServiceContract
{
# region Events
public class MessageEventArgs : EventArgs
{
public string Message;
public MessageEventArgs(string msg)
{
Message = msg;
}
}
public delegate void MessageEventHandler(object obj, MessageEventArgs e);
public event MessageEventHandler ProcessedAction;
#endregion
List<MyClient> _clients;
object locker = new object();
public MyService()
{
_clients = new List<MyClient>();
}
IMyServiceCallback GetCurrentCallback()
{
return OperationContext.Current.GetCallbackChannel<IMyServiceCallback>();
}
public void Connect(string user)
{
if ((user != null) && (user != ""))
{
MyClient client = new MyClient();
try
{
client.Username = user;
client.ClientCallback = this.GetCurrentCallback();
}
catch
{
}
lock (locker)
{
foreach (MyClient c in _clients)
{
if (c.Username == user)
{
_clients.Remove(c);
break;
}
}
_clients.Add(client);
}
if (ProcessedAction != null)
{
string msg = "Connection received from: " + user;
ProcessedAction(this, new MessageEventArgs(msg));
}
}
}
public void SiteChanged()
{
List<MyClient> missingClients = new List<MyClient>();
string msg;
if ((_clients != null) && (_clients.Count > 0))
{
foreach (MyClient c in _clients)
{
if ((c.ClientCallback != null))
{
try
{
c.ClientCallback.MyCallback();
msg = "Called " + c.Username;
}
catch (Exception e)
{
msg = c.Username + " failed to respond";
missingClients.Add(c);
}
if (ProcessedAction != null)
{
ProcessedAction(this, new MessageEventArgs(msg));
}
}
}
if (missingClients.Count > 0)
{
foreach (MyClient c in missingClients)
{
_clients.Remove(c);
msg = "Removed " + c.Username;
if (ProcessedAction != null)
{
ProcessedAction(this, new MessageEventArgs(msg));
}
}
}
}
}
}
public partial class MainWindow : Window
{
ServiceHost selfHost;
MyService _wcfService;
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
WSDualHttpBinding dualHttpBinding = new WSDualHttpBinding(WSDualHttpSecurityMode.None);
dualHttpBinding.Security.Message.ClientCredentialType = MessageCredentialType.None;
dualHttpBinding.Security.Message.NegotiateServiceCredential = false;
_wcfService = new MyService();
_wcfService.ProcessedAction += new MyService.MessageEventHandler(_wcfService_ProcessedAction);
Uri _baseAddress = new Uri("http://localhost:8000/WCFService/Service/");
selfHost = new ServiceHost(_wcfService, _baseAddress);
selfHost.Credentials.WindowsAuthentication.AllowAnonymousLogons = true;
ServiceEndpoint endpointAdmin = selfHost.AddServiceEndpoint(typeof(IMyServiceContract), dualHttpBinding, "Administrator");
ServiceEndpoint endpointClient1 = selfHost.AddServiceEndpoint(typeof(IMyServiceContract), dualHttpBinding, "Client1");
#if DEBUG
ServiceMetadataBehavior smb = new ServiceMetadataBehavior();
smb.HttpGetEnabled = true;
smb.HttpGetUrl = new Uri("http://localhost:8732/WCFService");
selfHost.Description.Behaviors.Add(smb);
#endif
try
{
selfHost.Open();
}
catch (Exception exc)
{
}
}
void _wcfService_ProcessedAction(object obj, MyService.MessageEventArgs e)
{
txtActivity.AppendText(e.Message + "\r");
}
private void btnCall_Click(object sender, RoutedEventArgs e)
{
if (_wcfService != null)
{
txtActivity.AppendText("Call button pressed\r");
_wcfService.SiteChanged();
}
}
}
}
CLIENT SIDE
.xaml Code
<Page x:Class="MyBrowserApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
Title="MainPage" Loaded="pgMain_Loaded">
<Grid>
<TextBox Margin="12,68,12,12" Name="txtActivity" TextWrapping="Wrap" />
<Button Content="Login" DataContext="{Binding}" Height="23" HorizontalAlignment="Left" Margin="10,39,0,0" Name="btnLogin" VerticalAlignment="Top" Width="75" Click="btnLogin_Click" />
<TextBox Height="23" HorizontalAlignment="Left" Margin="10,10,0,0" Name="txtUser" Text="Enter Username..." VerticalAlignment="Top" Width="120" GotFocus="txtUser_GotFocus" />
</Grid>
</Page>
Source Code for Client
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ServiceModel;
using MyBrowserApp.ServiceReference1;
namespace MyBrowserApp
{
public partial class MainPage : Page
{
string _userName, host;
MyServiceCallbackHandler myCallback;
public MainPage()
{
InitializeComponent();
}
private void pgMain_Loaded(object sender, RoutedEventArgs e)
{
Uri source = System.Windows.Interop.BrowserInteropHelper.Source;
host = source.Host;
if ((host == null) || (host == ""))
{
host = "localhost";
}
myCallback = new MyServiceCallbackHandler();
myCallback.ProcessedActivity += new MyServiceCallbackHandler.MessageEventHandler(myCallback_ProcessedActivity);
}
void myCallback_ProcessedActivity(object obj, MyServiceCallbackHandler.MessageEventArgs e)
{
txtActivity.AppendText(e.Message + "\r");
}
private void btnLogin_Click(object sender, RoutedEventArgs e)
{
_userName = txtUser.Text;
try
{
txtActivity.AppendText("Open connection\r Host: " + host + "\r User: " + _userName + "\r");
myCallback.Open(host, _userName);
}
catch (Exception exc)
{
txtActivity.AppendText(exc.Message + "\r");
}
}
private void txtUser_GotFocus(object sender, RoutedEventArgs e)
{
if (txtUser.Text == "Enter Username...")
{
txtUser.Text = "";
}
}
}
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple)]
class MyServiceCallbackHandler : IMyServiceContractCallback
{
#region Events
public class MessageEventArgs : EventArgs
{
public string Message;
public MessageEventArgs(string msg)
{
Message = msg;
}
}
public delegate void MessageEventHandler(object obj, MessageEventArgs e);
public event MessageEventHandler ProcessedActivity;
#endregion
private MyServiceContractClient myServiceClient;
private string _username;
public string Username
{
get
{
return _username;
}
set
{
_username = value;
}
}
public void Open(string host, string user)
{
string endpoint = "http://"+host+":8000/WCFService/Service/"+user;
string clientBaseAddress = "http://localhost:8000/WCFService/Client/" + user;
InstanceContext instanceContext = new InstanceContext(this);
WSDualHttpBinding dualHttpBinding = new WSDualHttpBinding(WSDualHttpSecurityMode.None);
dualHttpBinding.ClientBaseAddress = new Uri(clientBaseAddress);
myServiceClient = new MyServiceContractClient(instanceContext, dualHttpBinding, new EndpointAddress(endpoint));
myServiceClient.ClientCredentials.SupportInteractive = true;
_username = user;
string msg;
if (_username != null)
{
try
{
myServiceClient.Connect(_username);
msg = "Connect as user: " + _username;
}
catch (Exception exc)
{
msg = exc.Message;
}
if (ProcessedActivity != null)
{
ProcessedActivity(this, new MessageEventArgs(msg));
}
}
else
{
if (ProcessedActivity != null)
{
msg = "Username is null!";
ProcessedActivity(this, new MessageEventArgs(msg));
}
}
}
#region IMyServiceContractCallback Members
public void MyCallback()
{
string msg = "Callback received";
if (ProcessedActivity != null)
{
ProcessedActivity(this, new MessageEventArgs(msg));
}
}
#endregion
}
}
Note: In order to get "ServiceReference1" you need to run the service side. Then, add a service reference to the client side and leave it named as the default ServiceReference1.
Also, to fully understand the environment I am trying to execute it is necessary to publish the client application and then run two instances of it. The first instance type "Administrator" into the text box. In the second instance type "Client1"
The examples that I have found indicate that keeping a list of callbacks should give me the functionality I need but I only ever report to the last client to connect by the number times directly proportional to the number of clients to connect.
I originally wanted to use NetTcp binding, which means that the clientBaseAddress does not apply in the code I provided above. However, it gave the same results as described above. So, I switched to using the WSDualHttp binding thinking that being able to define the clientBaseAddress would resolve my issue and it did not.
I'm sorry for the long winded question but I am at a loss and really need to figure this out. I will be happy to provide any additional information anyone needs to get to the bottom of this behavior.