Click here to Skip to main content
14,458,335 members
Rate this:
Please Sign up or sign in to vote.
See more:
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);
    }

    // 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
<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
{
    /// <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.
Posted
Updated 25-Sep-13 11:41am
v2
Comments
Matt T Heffron 24-Sep-13 20:43pm
   
The SiteChanged() method will not compile as shown!
You have two nested foreach loops on the same control variable!
foreach (MyClient c in ...)
Also, missingClients is initialized as an empty List but is never updated.
So...what is the real code here? So we can really help.
TRedlich84 25-Sep-13 16:44pm
   
Apologies while trying to trim the code and reformat it to look appropriate on the site I messed it up.. ugh... Anyway, I have fixed it and at your request provided the full application code.
Matt T Heffron 25-Sep-13 17:08pm
   
OK, despite anything else that may be wrong, as G3Coder pointed out yesterday, static here IS going to be a problem:
private static IMyServiceCallback _clientCallback;
It guarantees that all instances of MyClient will share a single ClientCallback, the last one set.
And it will be called once for each instance of MyClient in the _clients list.
That sounds a lot like your symptom...
TRedlich84 25-Sep-13 17:51pm
   
I could have swore that I tested that. Clearly not. It was simple and sadly much more so than I had originally thought. Thank you for your help

1 solution

Rate this:
Please Sign up or sign in to vote.

Solution 1

Hi,

Could it be that the static IMyServiceCallback _clientCallback is causing the unexpected behaviour?

Good luck.

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; }
       }
   }
   
Comments
TRedlich84 24-Sep-13 17:43pm
   
You were absolutely correct. This is that humbling moment where you wish you hadn't overlooked something so simple. I thought I had tested that. Clearly not and through all the renditions of code changes I probably fixed and broke it at the same time. Thank you for the feedback
G3Coder 30-Sep-13 9:48am
   
Hi,

No problem - most of the time all it takes is a different pair of eyes.

Good luck.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)




CodeProject, 503-250 Ferrand Drive Toronto Ontario, M3C 3G8 Canada +1 416-849-8900 x 100