Alan Wayne
Alan Wayne

Reputation: 5384

How to subscribe to an F# event from a C# WPF usercontrol code-behind

in F#, I have the event defined as:

type ProgressNoteEvent() =
    
        let event1 = new Event<string>()
        let standardDotNetEventArgsEvent = new Event<EventHandler, EventArgs>()
    
        [<CLIEvent>]
        member this.Event1 = event1.Publish

        [<CLIEvent>]
        member this.StandardDotNetEventArgsEvent = standardDotNetEventArgsEvent.Publish
    
        member this.TestEvent(arg) =
            event1.Trigger(arg)

        member this.TestStandardDotNetEventArgsEvent() =
            standardDotNetEventArgsEvent.Trigger(this, EventArgs.Empty)

In the C# usercontrol code-behind, I have:

 public ProgressNoteEvent progressNoteEvent;

  progressNoteEvent = new ProgressNoteEvent();
  progressNoteEvent.Event1 += ProgressNoteEvent_Event1;
  progressNoteEvent.StandardDotNetEventArgsEvent += ProgressNoteEvent_StandardDotNetEventArgsEvent;

These events are raised by F# (in the DataContext for the WPF usercontrol):

let classWithEvent = new ProgressNoteEvent()
classWithEvent.Event1.Add(fun arg ->printfn "Event1 occurred! Object data: %s" arg)             
classWithEvent.TestEvent("Hello World!")

classWithEvent.TestStandardDotNetEventArgsEvent()

I'm clearly missing something...The C# code in the code-behind never receives the raised event. What am I missing?

TIA

Upvotes: 0

Views: 73

Answers (2)

Alan Wayne
Alan Wayne

Reputation: 5384

@TomasPetricek is 100% correct.

Here is what I ended up doing (for any newbie like me :)

The reason behind this is I needed a way for my F# backend code to modify the wpf usercontrol being displayed to the user--without depending on a simple property update and without wanting to constantly change a dependency property in the C# UserControl. So briefly, please make note of the following:

  • I am using WPF/C# as the frontend with UserControls.
  • The backend is F# for all the business logic and access to the server database.
  • I am using standard wpf Bindings to transfer data to the backend F# code.
  • The Bindings are supported by Elmish.WPF (which makes things really easy).
  • I needed a simple way of having the F# business logic change the user control.

My solution is a simple implementation of the Mediator Pattern (in C#).

Here are the snippets needed in the XAML:

 xmlns:i="http://schemas.microsoft.com/xaml/behaviors" 

 <i:Interaction.Triggers>
      <i:EventTrigger EventName="SubscribeToEventMediator">
            <i:InvokeCommandAction PassEventArgsToCommand="True" Command="{Binding SubscribeToEventMediator}" CommandParameter="{Binding EventMediator, ElementName=MyProgressNoteWriter}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

In the Code-Behind:

In the constructor of the usercontrol:

 this.Loaded += ProgressNoteWriter_Loaded;

(Sending the EventMediator from the constructor did not work). Then,

private void ProgressNoteWriter_Loaded(object sender, RoutedEventArgs e)
        {
            EventMediator = new ProgressNoteEvent();
            EventMediator.Event1 += ProgressNoteEvent_Event1;
            EventMediator.StandardDotNetEventArgsEvent += ProgressNoteEvent_StandardDotNetEventArgsEvent;
            ((FrameworkElement)sender).RaiseEvent(new RoutedEventArgs(SubscribeToEventMediatorEvent));
        }

        private void ProgressNoteEvent_StandardDotNetEventArgsEvent(object sender, EventArgs e)
        {
            throw new NotImplementedException();
        }

        private void ProgressNoteEvent_Event1(object sender, string args)
        {
            throw new NotImplementedException();
        }

 public static readonly RoutedEvent SubscribeToEventMediatorEvent = EventManager.RegisterRoutedEvent(
          name: "SubscribeToEventMediator",
          routingStrategy: RoutingStrategy.Bubble,
          handlerType: typeof(RoutedEventHandler),
          ownerType: typeof(ProgressNoteWriter));
        public event RoutedEventHandler SubscribeToEventMediator
        {
            add { AddHandler(SubscribeToEventMediatorEvent, value); }
            remove { RemoveHandler(SubscribeToEventMediatorEvent, value); }
        }

 // the EventMediator will be passed to F#. MUST USE THE SAME INSTANCE FOR EVENT COMMUNICATION.
        public ProgressNoteEvent EventMediator
        {
            get { return (ProgressNoteEvent)GetValue(EventMediatorProperty); }
            set { SetValue(EventMediatorProperty, value); }
        }

        // Using a DependencyProperty as the backing store for EventMediator.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty EventMediatorProperty =
            DependencyProperty.Register("EventMediator", typeof(ProgressNoteEvent), typeof(ProgressNoteWriter), new PropertyMetadata(null));

Then in F# (via Elemish.WPF)

"SubscribeToEventMediator" |> Binding.cmdParam (fun p m ->SubscribeToEventMediator (p:?> ProgressNoteEvent ))

 let update msg m =
           match msg with 
  | SubscribeToEventMediator e -> {m with EventMediator = Some e}, Cmd.none

At this point, the same instance of the EventMediator is now in both the C# Usercontrol and F#. Raising the events as first written works correctly:

let SendEvent =
        match m.EventMediator with
        | None -> ()
        | Some mediator -> mediator.Event1.Add(fun arg -> printfn "Event1 occurred! Object data: %s" arg)
                                                                   mediator.TestEvent("Hello World!")
                                                                   mediator.TestStandardDotNetEventArgsEvent()

                                             SendEvent

I hope this helps somebody :)

Upvotes: 0

Tomas Petricek
Tomas Petricek

Reputation: 243051

From your code sample, it seems you are creating two separate instances of the ProgressNoteEvent class:

  • one in C# (progressNoteEvent = new ProgressNoteEvent())
  • and another in F# (let classWithEvent = new ProgressNoteEvent())

As those are two separate instances, triggering events in one would not cause events in the other. For this to work, the C# code needs to access the same instance as the F# code. Without knowing more about the rest of your project, it is hard to say how to best do this - but you need to create just one instance and pass it to the other project (probably create one in C# and pass it to the F# code). You could also make this global or the event static, but that is probably not going to make your code very elegant.

Upvotes: 3

Related Questions