3

我想在属性编辑器中实现从选定对象到属性条目的映射。例如,在 Visual Studio xaml 编辑器中。

目标地图是这样的(或者可能使用 ReactiveUI 中的 ReactiveCollection?)

 Selected objects             Filled categories to display in PropertyEditor  
 -------------------------    ---------------------------------------
 ObservableCollection<obj> -> ObservableCollection<Category>

简单的英文地图:

  1. 从对象中收集所有独特的属性类型
  2. 按类别分组(例如文本、布局)
  3. 根据需要添加/删除类别以反映所选对象
  4. 根据需要从现有类别中添加/删除属性

挑战在于没有添加/删除分支的声明性/功能性代码。(我确实已经有一个基于命令/事件的代码,它非常丑陋且容易出错。)

我认为我们可以假设 Category 和 Property 集合是具有通常操作的集合:Union、Substract 和 IsMember。

灵感来自Paul Betts 的ReactiveUI代码,它非常适合简单的一对一映射:

var Models = new ReactiveCollection<ModelClass>();
var ViewModels = Models.CreateDerivedCollection(x => new ViewModelForModelClass(x));
// Now, adding / removing Models means we
// automatically have a corresponding ViewModel
Models.Add(new Model(”Hello!”));
ViewModels.Count();
>>> 1

使用 Seq 和 F#,直接的不可观察映射如下所示:

selectedObjects
|> Seq.collect GetProperties |> Seq.unique |> Seq.groupBy GetPropertyCategory
|> Seq.map (fun categoryName properies -> CreateCategory(properties))

上面的代码在理论上很好,但实际上它会在所选对象的每次更改时从头开始重新创建所有视图模型。我希望使用 Rex 实现的是将上述地图的版本与增量更新相结合,因此 WPF 将仅更新 GUI 的更改部分。

4

1 回答 1

4

这是一个非常有趣的问题:-)。遗憾的是,我认为没有任何 F# 库可以让您轻松解决这个问题。据我所知,Bindable LINQSelect是实现 LINQ 查询模式(即SelectManyWhere方法)的一种尝试ObservableCollection<T>,这基本上是您所需要的。要从 F# 中使用它,您可以将 LINQ 操作包装在诸如mapetc之类的函数中。

正如你所说,函数就像Seq.map工作一样IEnumerable,所以它们不是增量的。您在这里需要的是像map,filter和之类的函数collect,但是ObservableCollection<'T>以这样一种方式实现了它们以增量方式进行更改 - 当新元素添加到源集合中时,该map函数将处理它并将新元素添加到结果集合中.

我对此进行了一些实验,这是一个map增量的实现:

open System.Collections.Specialized
open System.Collections.ObjectModel

module ObservableCollection =
  /// Initialize observable collection
  let init n f = ObservableCollection<_>(List.init n f)

  /// Incremental map for observable collections
  let map f (oc:ObservableCollection<'T>) =
    // Create a resulting collection based on current elements
    let res = ObservableCollection<_>(Seq.map f oc)
    // Watch for changes in the source collection
    oc.CollectionChanged.Add(fun change ->
      printfn "%A" change.Action
      match change.Action with
      | NotifyCollectionChangedAction.Add ->
          // Apply 'f' to all new elements and add them to the result
          change.NewItems |> Seq.cast<'T> |> Seq.iteri (fun index item ->
            res.Insert(change.NewStartingIndex + index, f item))
      | NotifyCollectionChangedAction.Move ->
          // Move element in the resulting collection
          res.Move(change.OldStartingIndex, change.NewStartingIndex)
      | NotifyCollectionChangedAction.Remove ->
          // Remove element in the result
          res.RemoveAt(change.OldStartingIndex)
      | NotifyCollectionChangedAction.Replace -> 
          // Replace element with a new one (processed using 'f')
          change.NewItems |> Seq.cast<'T> |> Seq.iteri (fun index item ->
            res.[change.NewStartingIndex + index] <- f item)
      | NotifyCollectionChangedAction.Reset ->
          // Clear everything
          res.Clear()
      | _ -> failwith "Unexpected action!" )
    res

实现map起来很容易,但恐怕功能collectgroupBy很棘手。无论如何,这里有一个示例,展示了如何使用它:

let src = ObservableCollection.init 5 (fun n -> n)
let res = ObservableCollection.map (fun x -> printfn "processing %d" x; x * 10) src

src.Move(0, 1)
src.Remove(0)
src.Clear()
src.Add(5)
src.Insert(0, 3)
src.[0] <- 1

// Compare the original and the result
printfn "%A" (Seq.zip src res |> List.ofSeq)
于 2012-08-22T21:58:59.873 回答