tags:

views:

170

answers:

4

I have a Sender class that sends a Message on a IChannel:

public class MessageEventArgs : EventArgs {
  public Message Message { get; private set; }
  public MessageEventArgs(Message m) { Message = m; }
}

public interface IChannel {
  public event EventHandler<MessageEventArgs> MessageReceived;
  void Send(Message m);
}

public class Sender {
  public const int MaxWaitInMs = 5000;
  private IChannel _c = ...;

  public Message Send(Message m) {
    _c.Send(m);
    // wait for MaxWaitInMs to get an event from _c.MessageReceived
    // return the message or null if no message was received in response
  }
}

When we send messages, the IChannel sometimes gives a response depending on what kind of Message was sent by raising the MessageReceived event. The event arguments contain the message of interest.

I want Sender.Send() method to wait for a short time to see if this event is raised. If so, I'll return its MessageEventArgs.Message property. If not, I return a null Message.

How can I wait in this way? I'd prefer not to have do the threading legwork with ManualResetEvents and such, so sticking to regular events would be optimal for me.

+1  A: 

Have you tried assigning the function to call asynchronously to a delegate, then invoking the mydelegateinstance.BeginInvoke?

Linky for reference.

With the below example, just call

FillDataSet(ref table, ref dataset);

and it'll work as if by magic. :)

#region DataSet manipulation
///<summary>Fills a the distance table of a dataset</summary>
private void FillDataSet(ref DistanceDataTableAdapter taD, ref MyDataSet ds) {
  using (var myMRE = new ManualResetEventSlim(false)) {
    ds.EnforceConstraints = false;
    ds.Distance.BeginLoadData();
    Func<DistanceDataTable, int> distanceFill = taD.Fill;
    distanceFill.BeginInvoke(ds.Distance, FillCallback<DistanceDataTable>, new object[] { distanceFill, myMRE });
    WaitHandle.WaitAll(new []{ myMRE.WaitHandle });
    ds.Distance.EndLoadData();
    ds.EnforceConstraints = true;
  }
}
/// <summary>
/// Callback used when filling a table asynchronously.
/// </summary>
/// <param name="result">Represents the status of the asynchronous operation.</param>
private void FillCallback<MyDataTable>(IAsyncResult result) where MyDataTable: DataTable {
  var state = result.AsyncState as object[];
  Debug.Assert((state != null) && (state.Length == 2), "State variable is either null or an invalid number of parameters were passed.");

  var fillFunc = state[0] as Func<MyDataTable, int>;
  var mre = state[1] as ManualResetEventSlim;
  Debug.Assert((mre != null) && (fillFunc != null));
  int rowsAffected = fillFunc.EndInvoke(result);
  Debug.WriteLine(" Rows: " + rowsAffected.ToString());
  mre.Set();
}
Geoff
+1: good call, IMO. Basically implements `ManualResetEvent` for you.
dboarman
I'm not sure I understand this answer. Can you illustrate what you mean in the context of this example?
Evan Barkley
I'll update it with an example from my own code (which works flawlessly) 2 seconds..
Geoff
Hmm.. I appreciate the concrete example you provided, but this is still pretty cryptic to me. And this seems to use ManualResetEvents directly, too. =/ This example also doesn't seem to be analogous to the situation in my example, given that there's a call to `EndLoadData` which appears to do some hidden things.
Evan Barkley
There's no way around the reset events that I can see (whatever type they may be), what you're asking for requires threading by it's very nature, so it's unavoidable. There's nothing to be afraid of, especially with this example - that's the beauty of this design pattern I'm demonstrating - it's entirely self-contained, no threading issues (it hides them behind the async call)!
Geoff
I had forgotten to take that call to Endloaddata out for the example, sorry, since you saw it I added the beginloaddata too :)
Geoff
@Geoff: Gotcha. Just to be clear, your proposal is that in my `Sender.Send` method, I do the following: (1) assign the `IChannel.Send` call to a delegate, (2) then asynchronously invoke that with `BeginInvoke`, (3) pass both a wait handle and a reference to the delegate being invoked, (4) then `WaitOne` on the wait handle, and (5) continue along once the `.Set` is called in the delegate?
Evan Barkley
@Evan: That wouldn't work though, what would call `Set`? If you called it in the delegate how would you guarantee any sort of wait? And if you always `Sleep(timeout)` you'll sleep no matter what, defeating all the complicated threading.
Ron Warholic
@Evan: Yep.@Ron: You don't need to wait if you don't have to, that's the whole point! :) the .Set only gets called after EnvInvoke because EndInvoke is a blocking call. This means .Set will be called at the earliest possible moment.
Geoff
+2  A: 

Use a AutoResetEvent.

Gimme a few minutes and I'll throw together a sample.

Here it is:

public class Sender
{
    public static readonly TimeSpan MaxWait = TimeSpan.FromMilliseconds(5000);

    private IChannel _c;
    private AutoResetEvent _messageRecieved;

    public Sender()
    {
        // initialize _c
        this._messageRecieved = new AutoResetEvent(false);
        this._c.MessageReceived += this.MessageReceived;
    }

    public Message Send(Message m)
    {
        this._c.Send(m);
        // wait for MaxWaitInMs to get an event from _c.MessageReceived
        // return the message or null if no message was received in response


        // This will wait for up to 5000 ms, then throw an exception.
        this._messageRecieved.WaitOne(MaxWait);

        return null;
    }

    public void MessageReceived(object sender, MessageEventArgs e)
    {
        //Do whatever you need to do with the message

        this._messageRecieved.Set();
    }
}
Toby
I know that I can do this with a ManualResetEvent, but like I said ("I'd prefer not to have do the threading legwork with `ManualResetEvents`"), I'm hoping there's another option.
Evan Barkley
Sorry, I missed that in your original post.Even if you create a delegate and call `BeginInvoke`, you still have to have a callback method, and you'll still have to manually synchronize the threads somehow.Although, the IAsyncResult instance should provide a WaitHandle for you, which would simplify things a slight bit, but not much.
Toby
This looks like it will do the trick, and it's about as straightforward as I can get, I suppose. Thanks!
Evan Barkley
@Toby: One more question: using this method, it looks like I'd have to have `Sender.MessageReceived` store some data (say, in an instance field) so that `Sender.Send` can return it later. (To see this, try figuring out how you'd return something other than null from `Sender.Send`.) Is there a self-contained way to do it from within the `Sender.Send` method?
Evan Barkley
A: 

Perhaps your MessageReceived method should simply flag a value to a property of your IChannel interface, while implementing the INotifyPropertyChanged event handler, so that you would be advised when the property is changed.

By doing so, your Sender class could loop until the max waiting time is elapsed, or whenever the PropertyChanged event handler occurs, breaking the loop succesfully. If your loop doesn't get broken, then the message shall be considered as never received.

Will Marcouiller
That would work, but it would require changing the `IChannel` contract, which seems less than desirable. From a design standpoint, I don't think IChannel should have to change given that nothing about the way its implementers are using it has changed here.
Evan Barkley
A: 

WaitOne is really the right tool for this job. In short, you want to wait between 0 and MaxWaitInMs milliseconds for a job to complete. You really have two choices, poll for completion or synchronize the threads with some construct that can wait an arbitrary amount of time.

Since you're well aware of the right way to do this, for posterity I'll post the polling version:

MessageEventArgs msgArgs = null;
var callback = (object o, MessageEventArgs args) => {
    msgArgs = args;
};

_c.MessageReceived += callback;
_c.Send(m);

int msLeft = MaxWaitInMs;
while (msgArgs == null || msLeft >= 0) {
    Thread.Sleep(100);
    msLeft -= 100; // you should measure this instead with say, Stopwatch
}

_c.MessageRecieved -= callback;
Ron Warholic