F#新手求教:如何实现C#的RelayCommand及WeakAction逻辑
Hey there! Since you're new to F# and working through porting a C# app as a learning exercise, let's walk through how to implement a RelayCommand equivalent—including handling weak references just like your existing C# WeakFunc/WeakAction code.
Implementing a Basic RelayCommand in F#
First, let's start with a simple, straightforward RelayCommand that mirrors the core behavior of your C# version (without weak references). This works great for scenarios where you don't need to worry about memory leaks (like static commands or short-lived view models):
open System open System.Windows.Input type RelayCommand(execute: obj -> unit, canExecute: obj -> bool) = // Event for CanExecuteChanged notifications let canExecuteChanged = Event<EventHandler, EventArgs>() interface ICommand with // Expose the event as a CLI-compatible event [<CLIEvent>] member _.CanExecuteChanged = canExecuteChanged.Publish member _.CanExecute(parameter) = canExecute parameter member _.Execute(parameter) = execute parameter // Helper method to trigger CanExecuteChanged (matches C#'s RaiseCanExecuteChanged) member _.RaiseCanExecuteChanged() = canExecuteChanged.Trigger(this, EventArgs.Empty)
If you don't need a CanExecute check, you can add an overload for convenience:
type RelayCommand(execute: obj -> unit) = inherit RelayCommand(execute, fun _ -> true)
Adding Weak Reference Support (Like C#'s WeakFunc/WeakAction)
To replicate the weak reference behavior of your C# WeakFunc/WeakAction (and prevent memory leaks between views and view models), we'll wrap the command delegates in weak references. This ensures that if the target object (like a view model) is garbage collected, the command won't keep it alive.
First, let's create a helper type to wrap weak references to delegates:
open System open System.Reflection // Helper to hold a weak reference to a delegate's target and method info type private WeakDelegate<'TArg>(target: obj, method: MethodInfo) = let weakTarget = WeakReference(target) member _.Invoke(arg: 'TArg) = match weakTarget.Target with | null -> // Target was garbage collected—don't execute false | aliveTarget -> // Invoke the method on the alive target method.Invoke(aliveTarget, [| box arg |]) |> ignore true
Now we can build a WeakRelayCommand that uses this helper:
open System.Windows.Input type WeakRelayCommand(execute: obj -> unit, canExecute: obj -> bool option) = // Wrap the execute delegate in a weak reference let executeWeak = WeakDelegate<obj>(execute.Target, execute.Method) // Wrap the canExecute delegate (if provided) in a weak reference let canExecuteWeak = canExecute |> Option.map (fun ce -> WeakDelegate<obj>(ce.Target, ce.Method)) let canExecuteChanged = Event<EventHandler, EventArgs>() interface ICommand with [<CLIEvent>] member _.CanExecuteChanged = canExecuteChanged.Publish member _.CanExecute(parameter) = match canExecuteWeak with | Some ce -> ce.Invoke(parameter) | None -> true // Default to true if no CanExecute is provided member _.Execute(parameter) = ignore (executeWeak.Invoke(parameter)) member _.RaiseCanExecuteChanged() = canExecuteChanged.Trigger(this, EventArgs.Empty) // Overload for when you don't need a CanExecute check type WeakRelayCommand(execute: obj -> unit) = inherit WeakRelayCommand(execute, None)
Using the WeakRelayCommand
Here's how you'd use this in a view model, just like your C# code:
type CounterViewModel() = let mutable _count = 0 member _.Count = _count // Command that increments count, but only if count < 10 member _.IncrementCommand = WeakRelayCommand( (fun _ -> _count <- _count + 1), Some (fun _ -> _count < 10) ) :> ICommand // Command that resets count (no CanExecute check) member _.ResetCommand = WeakRelayCommand(fun _ -> _count <- 0) :> ICommand
How F# Handles WeakAction Logic
In C#, WeakAction and WeakFunc exist to hold weak references to delegates without keeping their target objects alive. In F#, we achieve the same effect using:
WeakReference<T>(or the non-genericWeakReference) to hold a weak reference to the delegate's target object.MethodInfoto store the method we want to invoke, so we can call it later if the target is still alive.
The WeakDelegate helper above mirrors the behavior of your C# WeakFunc:
- It stores a weak reference to the target object.
- When you call
Invoke, it checks if the target is still alive. - If it is, it uses reflection to invoke the method on the target.
- If not, it skips execution (just like a C# WeakAction that's been garbage collected).
This approach ensures that your commands don't create memory leaks by holding strong references to view models or views that should be collected.
内容的提问来源于stack exchange,提问作者Alan Wayne

