Search code examples
f#quotations

How to get property value from property-getter quotation


TL;DR: How do I get the actual property value from a quotation of the form

<@ myInstance.myProperty @>

I'm trying to simplify INotifyPropertyChanged using F#. Instead of subscribing directly to PropertyChanged, I want to use a method that takes a code quotation containing the property I want to subscribe to (e.g. <@ vm.IsChanged @>) and a callback (or alternatively just the quotation and returns an observable of the relevant property). For example:

type MyVm() =
  inherit INPCBaseWithObserveMethod()
  ...

let vm = new MyVm()
vm.Observe <@ vm.IsChanged @> (fun isChanged -> ...)

I'm new to code quotations and I'm struggling with the implementation of the Observe method. I know how to get the property name from this kind of expression, but not the value. Here's what I have so far (note the placeholder in propInfo.GetValue):

type ViewModelBase() =

  // Start INPC boilerplate

  let propertyChanged = new Event<_, _>()

  interface INotifyPropertyChanged with
    [<CLIEvent>]
    member __.PropertyChanged = propertyChanged.Publish

  member this.OnPropertyChanged(propertyName : string) =
      propertyChanged.Trigger(this, new PropertyChangedEventArgs(propertyName))

  // End INPC boilerplate

  member this.Observe (query: Expr<'a>) (callback: 'a -> unit) : unit = 
    match query with
    | PropertyGet(instanceExpr, propInfo, _) ->
        (this :> INotifyPropertyChanged).PropertyChanged
        |> Observable.filter (fun args -> args.PropertyName = propInfo.Name)
        |> Observable.map (fun _ -> propInfo.GetValue(TODO) :?> 'a)
        |> Observable.add callback
    | _ -> failwith "Expression must be a non-static property getter"

Solution

  • I figured it out based on some experimentation and this quotation eval function. In the most simple case (when vm in <@ vm.MyProperty @> is a local let-bound value), the instance expression will match the Value pattern:

    | PropertyGet(Some (Value (instance, _)), propInfo, [])
    

    instance can then be passed to PropertyInfo.GetValue. However, if vm is a field (class-level let binding) or anything else, then the pattern will be different (e.g. containing a nested FieldGet which will need to be evaluated to get the correct instance you can pass to PropertyInfo.GetValue).

    In short, it seems the best course of action is simply using the eval function I linked to. The whole ViewModelBase class then becomes (see this snippet for a more complete implementation):

    type ViewModelBase() =
    
      /// Evaluates an expression. From http://www.fssnip.net/h1
      let rec eval = function
        | Value (v, _) -> v
        | Coerce (e, _) -> eval e
        | NewObject (ci, args) -> ci.Invoke (evalAll args)
        | NewArray (t, args) -> 
            let array = Array.CreateInstance (t, args.Length) 
            args |> List.iteri (fun i arg -> array.SetValue (eval arg, i))
            box array
        | NewUnionCase (case, args) -> FSharpValue.MakeUnion (case, evalAll args)
        | NewRecord (t, args) -> FSharpValue.MakeRecord (t, evalAll args)
        | NewTuple args ->
            let t = FSharpType.MakeTupleType [| for arg in args -> arg.Type |]
            FSharpValue.MakeTuple (evalAll args, t)
        | FieldGet (Some (Value (v, _)), fi) -> fi.GetValue v
        | PropertyGet (None, pi, args) -> pi.GetValue (null, evalAll args)
        | PropertyGet (Some x, pi, args) -> pi.GetValue (eval x, evalAll args)
        | Call (None, mi, args) -> mi.Invoke (null, evalAll args)
        | Call (Some x, mi, args) -> mi.Invoke (eval x, evalAll args)
        | x -> raise <| NotSupportedException(string x)
      and evalAll args = [| for arg in args -> eval arg |]
    
      let propertyChanged = new Event<_,_>()
    
      interface INotifyPropertyChanged with
        [<CLIEvent>]
        member __.PropertyChanged = propertyChanged.Publish
    
      member this.OnPropertyChanged(propertyName : string) =
        propertyChanged.Trigger(this, new PropertyChangedEventArgs(propertyName))
    
      /// Given a property-getter quotation, calls the callback with the value of
      /// the expression every time INotifyPropertyChanged is raised for this property.
      member this.Observe (expr: Expr<'a>) (callback: 'a -> unit) : unit = 
        match expr with
        | PropertyGet (_, propInfo, _) ->
            (this :> INotifyPropertyChanged).PropertyChanged
            |> Observable.filter (fun args -> args.PropertyName = propInfo.Name)
            |> Observable.map (fun _ -> eval expr :?> 'a)
            |> Observable.add callback
        | _ -> failwith "Expression must be a property getter"
    

    Observe can of course be trivially modified to return an observable instead of subscribing directly.

    Note that in many scenarios Observable.DistinctUntilChanged might be desired after Observable.map. (I use Observe to, among other things, trigger animations from view model properties, and due assumptions in my particular animation code, the animations got all wonky when there were several subsequent calls to the callback with an unchanged property value.)