如何从 属性-getter 报价中获取 属性 值

How to get property value from property-getter quotation

TL;DR: 如何从

形式的引用中获取实际的 属性 值
<@ myInstance.myProperty @>

我正在尝试使用 F# 简化 INotifyPropertyChanged。我不想直接订阅 PropertyChanged,而是想使用一种方法,该方法采用包含我要订阅的 属性 的代码引用(例如 <@ vm.IsChanged @>)和回调(或者只是引用和 returns 相关 属性) 的可观察值。例如:

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

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

我是代码引用的新手,正在努力实现 Observe 方法。我知道如何从这种表达式中获取 属性 name,但不知道 value。这是我到目前为止的内容(注意 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"

我根据一些实验和 this quotation eval function 弄明白了。在最简单的情况下(当 <@ vm.MyProperty @> 中的 vm 是本地 let 绑定值时),实例表达式将匹配 Value 模式:

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

instance 然后可以传递给 PropertyInfo.GetValue。但是,如果 vm 是一个字段(class-level let 绑定)或其他任何东西,那么模式将不同(例如,包含一个嵌套的 FieldGet 需要进行评估以获得可以传递给 PropertyInfo.GetValue) 的正确实例。

简而言之,最好的做法似乎就是使用我链接到的 eval 函数。整个 ViewModelBase class 就变成了(更完整的实现参见 this snippet):

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 当然可以简单地修改为 return 一个可观察的而不是直接订阅。

请注意,在许多情况下,Observable.map 之后可能需要 Observable.DistinctUntilChanged。 (除其他外,我使用 Observe 从视图模型属性触发动画,并且在我的特定动画代码中进行了适当的假设,当随后多次调用未更改的回调时,动画变得非常不稳定 属性 值。)