Click here to Skip to main content
15,896,269 members

WCF Multiple Client Duplex

TRedlich84 asked:

Open original thread
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
XML
<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
C#
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);
    }

    // Callback Contract for the server to call on the clients
    public interface IMyServiceCallback
    {
        [OperationContract(IsOneWay = true)]
        void MyCallback();
    }

    /// <summary>
    /// Class object containing pertinent information about the Client that has
    /// called on the SiteStatus Service including the callback channel, user name,
    /// and TNAs being displayed at the remote site
    /// </summary>
    public class MyClient
    {
        public MyClient()
        {
            _clientCallback = null;
            _username = "";
        }

        // Callback to the client.
        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();

        // Default constructor
        public MyService()
        {
            _clients = new List<MyClient>();
        }


        /// <summary>
        /// This function is used to obtain the current callback for the calling
        /// client
        /// </summary>
        /// <returns>Current callback for the calling client</returns>
        IMyServiceCallback GetCurrentCallback()
        {
            return OperationContext.Current.GetCallbackChannel<IMyServiceCallback>();
        }

        public void Connect(string user)
        {
            if ((user != null) && (user != ""))
            {
                // Incoming connection from a client.
                MyClient client = new MyClient();

                try
                {
                    // Get the user name
                    client.Username = user;

                    // Get the callback from the client attempting to connect
                    client.ClientCallback = this.GetCurrentCallback();
                }
                catch
                {
                    // Failed to read the callback
                    // Exception Code
                }

                lock (locker)
                {
                    // Check to see if the user already exists
                    foreach (MyClient c in _clients)
                    {
                        if (c.Username == user)
                        {
                            // Remove the old client
                            _clients.Remove(c);
                            break;
                        }
                    }
                    // Add the new client to the list
                    _clients.Add(client);
                }

                if (ProcessedAction != null)
                {
                    string msg = "Connection received from: " + user;
                    ProcessedAction(this, new MessageEventArgs(msg));
                }

            }
        }

        /// <summary>
        /// This handles sending information to the client(s) regarding site
        /// changes.
        /// </summary>
        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)
                        {
                            // Exception Code...
                            msg = c.Username + " failed to respond";
                            missingClients.Add(c);
                        }
                        if (ProcessedAction != null)
                        {
                            ProcessedAction(this, new MessageEventArgs(msg));
                        }
                    }
                }

                if (missingClients.Count > 0)
                {
                    // At least one client connection has been
                    // lost since the last time we sent
                    // information. They need to be purged from
                    // the list of clients.
                    foreach (MyClient c in missingClients)
                    {
                        _clients.Remove(c);
                        msg = "Removed " + c.Username;
                        if (ProcessedAction != null)
                        {
                            ProcessedAction(this, new MessageEventArgs(msg));
                        }
                    }
                }
            }
        }
    }


    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    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);

            // Create the ServiceHost for the SiteMonitor
            Uri _baseAddress = new Uri("http://localhost:8000/WCFService/Service/");
            selfHost = new ServiceHost(_wcfService, _baseAddress);

            // Set the credentials for logging in
            selfHost.Credentials.WindowsAuthentication.AllowAnonymousLogons = true;

            // Add endpoints
            ServiceEndpoint endpointAdmin = selfHost.AddServiceEndpoint(typeof(IMyServiceContract), dualHttpBinding, "Administrator");
            ServiceEndpoint endpointClient1 = selfHost.AddServiceEndpoint(typeof(IMyServiceContract), dualHttpBinding, "Client1");


#if DEBUG
            // Enable metadata exchange. This is temporary for development. Needs to be
            // removed for production
            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)
            {
                // Exception Code
            }
        }

        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
XML
<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
C#
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
{
    /// <summary>
    /// Interaction logic for MainPage.xaml
    /// </summary>
    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; // Hostname of the client executing the program

            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)
            {
                // There was an error sending the service command
                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()
        {
            //... Code to process callback
            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.
Tags: C#, WCF

Plain Text
ASM
ASP
ASP.NET
BASIC
BAT
C#
C++
COBOL
CoffeeScript
CSS
Dart
dbase
F#
FORTRAN
HTML
Java
Javascript
Kotlin
Lua
MIDL
MSIL
ObjectiveC
Pascal
PERL
PHP
PowerShell
Python
Razor
Ruby
Scala
Shell
SLN
SQL
Swift
T4
Terminal
TypeScript
VB
VBScript
XML
YAML

Preview



When answering a question please:
  1. Read the question carefully.
  2. Understand that English isn't everyone's first language so be lenient of bad spelling and grammar.
  3. If a question is poorly phrased then either ask for clarification, ignore it, or edit the question and fix the problem. Insults are not welcome.
  4. Don't tell someone to read the manual. Chances are they have and don't get it. Provide an answer or move on to the next question.
Let's work to help developers, not make them feel stupid.
Please note that all posts will be submitted under the http://www.codeproject.com/info/cpol10.aspx.



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900