0

The end goal is to have a WCF server that calls a callback function, and each subscribed WCF client displays a new instance of a form on the callback.

To accomplish this, I am using using a pub/sub pattern where the clients connect to the server via a named pipe and call Subscribe(), then receive a callback, EventReceived() when an event is read from the event log. In the implementation for the callback, the clients instantiate a new instance of a form and show it.

The problem is that the forms that are shown by the client freeze immediately. Any suggestions would be helpful. A simplified version of the code which exhibits the same behavior is below.

Client Code (Form1 is just a blank, default form with no additional code/decorators. Form2's only purpose in life is to record the SynchronizationContext.Current for the callbacks to use later.)

static class Program
{
    public static SynchronizationContext SynchronizationContext;

    [STAThread]
    static void Main()
    {
        ClientContract callbacks = new ClientContract();

        DuplexChannelFactory<IEventService> pipeFactory = new DuplexChannelFactory<IEventService>(
            callbacks, new NetNamedPipeBinding(), new EndpointAddress("net.pipe://localhost/EventPipe"));

        IEventService pipeProxy = pipeFactory.CreateChannel();
        pipeProxy.Subscribe();


        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new Form2());
    }
}

[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Multiple, UseSynchronizationContext = false)]
class ClientContract : IClientContract
{
    public void EventReceived(int eventId)
    {
        Form1 newForm = new Form1();
        newForm.Show();
    }
}

Client Code - Form2 Constructor

public Form2()
{
    InitializeComponent();
    Program.SynchronizationContext = SynchronizationContext.Current;
}

Server Code

class Program
{
    static void Main(string[] args)
    {
        using (ServiceHost host = new ServiceHost(typeof(EventService), new Uri[] { new Uri("net.pipe://localhost") }))
        {
            host.AddServiceEndpoint(typeof(IEventService), new NetNamedPipeBinding(), "EventPipe");
            host.Open();
            Console.WriteLine("Service started");

            do
            {
                Console.WriteLine("Enter \"callback\" to trigger a callback, or enter to quit");
                string input = Console.ReadLine();

                if (input == "callback")
                {
                    EventService.PublishEvent(3);
                }
                else if (input == "")
                {
                    break;
                }
            } while (true);

            host.Close();
        }
    }
}

[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.PerCall)]
class EventService : IEventService
{
    public static event NewEventHandler NewEventAvailableHandler;
    public delegate void NewEventHandler(EventLogEventArgs e);

    private NewEventHandler newEventHandler;
    private IClientContract callback = null;

    public static void PublishEvent(int eventId)
    {
        EventLogEventArgs e = new EventLogEventArgs();
        e.eventId = eventId;

        if (NewEventAvailableHandler != null)
        {
            NewEventAvailableHandler(e);
        }
    }

    public void Subscribe()
    {
        callback = OperationContext.Current.GetCallbackChannel<IClientContract>();
        newEventHandler = new NewEventHandler(MagazineService_NewIssueAvailableEvent);
        NewEventAvailableHandler += newEventHandler;
    }

    public void Unsubscribe()
    {
        NewEventAvailableHandler -= newEventHandler;
    }

    public void MagazineService_NewIssueAvailableEvent(EventLogEventArgs e)
    {
        callback.EventReceived(e.eventId);
    }
}

public class EventLogEventArgs : EventArgs
{
    public int eventId;
}

Shared Code (implemented via a class library DLL file)

[ServiceContract(SessionMode = SessionMode.Required, CallbackContract = typeof(IClientContract))]
public interface IEventService
{
    [OperationContract(IsOneWay = true, IsInitiating = true)]
    void Subscribe();

    [OperationContract(IsOneWay = true, IsInitiating = true)]
    void Unsubscribe();
}

public interface IClientContract
{
    [OperationContract(IsOneWay = true)]
    void EventReceived(int eventId);
}

** edit **

As pointed out in the comments, the callback was received on a worker thread, but UI events need to happen on the main thread. I was able to get it to work, but it feels like a hack because I have a second, unused form, simply to get the SynchronizationContext for the callback to use.

Todd
  • 335
  • 3
  • 9
  • If you put a breakpoint on `Form1 newForm = new Form1();` inside `EventReceived(int eventId)`, what thread is it? Main/GUI thread? –  Feb 19 '16 at 01:33
  • It looks like a worker thread (see http://i.imgur.com/Lf1mViH.png). The Main thread blocks on Application.Run() in Main() and I am not doing anything explicit to create new threads (all code is in the original post). – Todd Feb 19 '16 at 02:17
  • OK. It's freezing because you can't create/update UI from a worker thread. Have your callback do a `BeginInvoke` or something on your application's main window to display the form on your behalf. –  Feb 19 '16 at 03:31
  • Well, actually you cannot update existing UI elements created in another thread when you're executing in the worker thread. But you can create completely new form - it will have its own dispatcher (when we're in WPF world) which barely means its own Windows message processing. More here: http://stackoverflow.com/questions/7568376/multiple-ui-threads-winforms Although this is possible it is not recommended to have such a situation in the application and you better post a request to create new window back to your main UI thread. – Igor Labutin Feb 19 '16 at 20:02
  • Thank you both for your suggestions. I was able to get it to work and updated the post, but the way I did it feels like a hack. I am using a second form that I don't even need, just to get the SynchronizationContext.Current within the Application.Run(), only to use later by the callbacks to display the forms I care about. Surely there's a better way? – Todd Feb 20 '16 at 17:35

0 Answers0