Search code examples
c#wpfwcfduplex

How to enable user GUI response in wcf service to trigger duplex callback to client


Maybe I am trying the impossible... I have created a wpf application to start a wcf service with the following service contract:

 [ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IRejectCallback))]
public interface IRejectService
{
    [OperationContract(IsOneWay = true)]
    void SubmitNewRejectInfo();

    [OperationContract(IsOneWay = true)]
    void SendRejectCallback();
}

My service behavior:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = true)]  //(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.Single, UseSynchronizationContext = true)]
public class RejectService : IRejectService, IPostRejectEvent

I create my duplex channel and call the SubmitNewRejectInfo service method:

    InstanceContext ic = new InstanceContext(new RejectCallbackHandler());
    tcpFactory = new DuplexChannelFactory<IRejectService>(ic, "netTcp");
    IRejectService _rejectService = tcpFactory.CreateChannel();
    _rejectService = _tcpFactory.CreateChannel();
    _rejectService.SubmitNewRejectInfo();

The SubmitNewRejectInfo method is run on the service side. Normally, I would add my callback method like this:

    public void SubmitNewRejectInfo(RejectInformation rejectInformation)
    {
        // Do something here...
        callback.RejectCallback();
    }

However, when the SubmitNewRejectInfo method is run from the client (using IsOneWay = true), I do not want to callback to the client at that time. I would like to wait for the user to click a button on my WPF GUI which will transmit the signal to callback to the client. ** Is it possible to postpone the callback, or send a callback via a different operation contract? **

How could the client invoke the service via an operation contract method and then receive a callback after user interaction happens on the service side? I saw one duplex example where someone used a reentrant service with Thread.Sleep() as follows:

public void Register()
{
    OperationContext ctxt = OperationContext.Current;
    IClientCallback callBack = ctxt.GetCallbackChannel<IClientCallback>();
    Thread.Sleep(3000); 
    callBack.TempUpdate(10);
}

In my case, I would need to trigger the callback in the Register method after a user clicks a button on a gui that is hosting the service. Would this be possible? Any ideas?

UPDATE **

I have discovered my main issue: I make a call from my wcf client to my operation service contract method:

        InstanceContext ic = new InstanceContext(new RejectCallbackHandler());
        _tcpFactory = new DuplexChannelFactory<IRejectService>(ic, "netTcp");
        _rejectService = _tcpFactory.CreateChannel();
        _rejectService.SubmitNewRejectInfo();

The wcf service operation is invoked here:

        public void SubmitNewRejectInfo(RejectInformation rejectInformation)
        {
          // Throw event to notify MainViewModel that new reject information is available.
           OnSubmitNewRejectInfo(new RejectInfoArgs(rejectInformation));
           callback.RejectCallback();
        }

The event is fired to notify my MainViewModel that some data has been updated and refresh some properties. Then problem begins... I do not want the callback.RejectCallback(); to fire just yet. I need the user to click a button on my Mainwindow GUI associated to the view model to "authorize" the duplex callback to return a message to the wcf client.

Any ideas how to "pause" the callback long enough for a user to click a button to authorize the duplex callback to deliver a message to the wcf client? Perhaps my OnSubmitNewRejectInfo event can return some event argument before the callback is invoked? Could a new delegate be triggered to return information from my MainViewModel before the callback is invoked?

I hope this describes my problem a little better. Any help is VERY much appreciated.

Update number 2 ** More information... :)

The WCF service was created as a WCF service class library. The WCF client was also created as a WCF service class library. This makes it easy for other applications or class objects to host the service and client. This was done in order for human interaction via a GUI on the service side, and other software interaction on the wcf client side. The WCF service and client are hosted on separate machines.

The WCF service is hosted by a WPF application, and communication is event driven between the two. The service class is created as a singleton in the MainViewModel of the WPF application.

The WCF service class must talk via duplex communication with the wcf client. The client invokes an operation contract to update information in the service, which is displayed on the WPF GUI. After the information is displayed on the GUI, then the user must click a button to invoke the callback to the client indicating that the service has completed it's task.

So, WPF app hosts a wpf service class library. There is communication between the WPF app and service class via events. The service class is consumed by a wcf client via duplex channel communication. The wcf client is also hosted by another class object with a service reference to the wcf service. The client communicates with it's host via events.

WCF CLIENT CODE:

        InstanceContext ic = new InstanceContext(new RejectCallbackHandler());
        _tcpFactory = new DuplexChannelFactory<IRejectService>(ic, "netTcp");
        _rejectService = _tcpFactory.CreateChannel();
        _rejectService.SubmitNewRejectInfo(); // This is where I invoke a service operation from my client.

WCF SERVICE CODE:

        // This service operation is consumed by the client.
        public void SubmitNewRejectInfo(RejectInformation rejectInformation)
        {
          // Create event to notify MainViewModel that new reject info is available.
           OnSubmitNewRejectInfo(new RejectInfoArgs(rejectInformation));

           // **** I need something to happen here in order to halt the duplex callback to the client until a human creates a button click event in my MainViewModel, which indicates the duplex callback may be sent back to the client. ****

           callback.RejectCallback();
        }

Sorry this question has become very detailed. I never should have fallen asleep during my technical writing class in college... :)

Update number 3 **

I tried running the code that degorolls mentioned below. His example code is perfect for my needs!! (Thanks degorolls!) However I get a null reference exception: "Object reference not set to an instance of an object".

First the action executes in this part of degoroll's demo code:

if (pendingNotifications.TryGetValue(rejectInformation, out action)) { try { action(rejectInformation); // This is invoked

Then this part of the demo code is called -> callback.RejectCallback(new RejectCallbackMessage())); :

    public void SubmitNewRejectInfo(RejectInformation rejectInformation)
    {
        // Throw event to notify MainViewModel that new reject information is available.
        OnSubmitNewRejectInfo(new RejectInfoArgs(rejectInformation));


        pendingNotifications.Add(rejectInformation, info => callback.RejectCallback(new RejectCallbackMessage()));   // **** the action returns to callback.RejectCallback here ****

This is where I get my null exception error.

Here is my code to get the callback channel:

    IRejectCallback callback
    {
        get { return OperationContext.Current.GetCallbackChannel<IRejectCallback>(); }
    }

My guess is that I am not returning null instead of the original callback channel...

Is there a way I can obtain the correct channel at this point in the code?


Solution

  • If I'm understanding things correctly it seems that the server simply needs to be keep a list of things it is waiting to do. How you implement will be tied closely to the instancing of the server. If you stick with singleton, you can simply hold a map of pending notification in the server class. E.g.:

    public class RejectService
    {
        Dictionary<RejectInformation, Action<RejectInformation>> pendingNotifications = new Dictionary<RejectInformation, Action<RejectInformation>>();
    
        public void SubmitNewRejectInfo(RejectInformation rejectInformation)
        {
           OnSubmitNewRejectInfo(new RejectInfoArgs(rejectInformation));
           pendingNotifications.Add(rejectInformation, info => callback.RejectCallback(info));
        }
    
        public void SendRejectCallback(RejectInformation rejectInformation)
        {
           Action<RejectInformation> action;
           if (pendingNotifications.TryGetValue(rejectInformation, out action))
           {
              acion(rejectInformation);
              pendingNotifications.Remove(rejectInformation);
           }
        }
    }
    

    If you want to make this reentrant you may need to think about locks... This is a really simplistic approach but gives a starting point.