双向视图模型与“实时”集合和属性同步

时间:2010-02-05 13:12:06

标签: wpf design-patterns mvvm

最近关于View Models(VM)的问题让我感到很紧张。

就像this guy一样,我得出的结论是,我需要在VM上公开的集合通常包含与业务对象上公开的集合不同的类型。

因此,这两种类型之间必须存在双向映射或转换。 (只是为了使事情复杂化,在我的项目中,这些数据是“实时”的,这样一旦你更改了一个属性,它就会被传输到其他计算机)

我可以使用像Truss之类的框架来处理这个概念,虽然我怀疑在某处会有一个令人讨厌的惊喜。

不仅必须转换对象,还需要在这两个集合之间进行同步。 (只是为了使我能想到的事情复杂化,VM集合可能是业务对象集合的子集或联合,而不仅仅是1:1的同步)。

我可以看到如何使用复制的ObservableCollection或像CLINQ这样的单向“实时”同步。

然后问题变成:创建/删除项目的最佳方法是什么?

双向直接同步似乎没有出现在卡片上 - 我没有找到这样的例子,唯一支持远程的类就是ListCollectionView。双向同步甚至是添加回业务对象集合的合理方式吗?

我见过的所有样品似乎都没有解决任何这种“复杂”的问题。

所以我的问题是:你如何解决这个问题?是否有一些技术可以从VM更新模型集合?对此最好的一般方法是什么?

6 个答案:

答案 0 :(得分:4)

就个人而言,我在我的模型和视图模型中使用了ObservableCollection。

class Model
{
    public ObservableCollection<Foo> Foos;
}

class ViewModel
{
    public Model Model;
    public ObservableCollection<FooView> Foos;

    public ViewModel()
    {
        Model.Foos.CollectionChanged += OnModelFoosCollection_CollectionChanged;
    }

    void OnModelFoosCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
       Foo re;

       switch (e.Action)
       {
          case NotifyCollectionChangedAction.Add:
             re = e.NewItems[0] as Foo;
             if (re != null)
                AddFoo(re);  //For other logic that may need to be applied
             break;
          case NotifyCollectionChangedAction.Remove:
             re = e.OldItems[0] as Foo;
             if (re != null)
                RemoveFoo(re); 
             break;
          case NotifyCollectionChangedAction.Reset:
             Foos.Clear();
             /* I have an AddRange in an ObservableCollection-derived class
                You could do Model.Foo.ForEach(ree => AddFoo(ree));
             */
             var converter = 
                from ree in Model.Foo
                select new FooView(ree);
             Reports.AddRange(converter); 
             break;
          default:
             //exercise for the reader :)
             s_ILog.Error("OnModelFoosCollection_CollectionChangedDid not deal with " + e.Action.ToString()); 
             break;
       }
    }   

    void AddFoo(Foo f)
    {
        Foos.Add(new FooView(f));
    }

    void RemoveFoo(Foo f)
    {
       var match = from f in Foos
          where f.Model == f  //if you have a unique id, that might be a faster comparison
          select f;
       if(match.Any())
          Foos.Remove(match.First());
    }
}

现在,当您从Model的Foo集合中删除某些内容时,它会自动删除相应的FooView。这与我对这种事情的看法相对应。如果我想删除某些内容,那么模型就是真正需要删除的地方。

感觉就像很多代码,但实际上并没有那么多。我确信可以构建一个通用版本,但IMO总是希望自定义逻辑处理元素的添加/删除。

答案 1 :(得分:3)

当您可能需要双向同步时,唯一的情况是,用于可视化VM集合的控件不会让您知道用户创建或删除项目的意图。即控件直接处理您的VM集合,并且您知道已添加/删除项目的唯一方法是监视VM的集合。如果不是这种情况,那么您可以实现单向同步并直接在模型的集合上添加/删除项目。

  

编辑:以WPF DataGrid为例   控制绑定到可观察的集合   ItemViewModels。如果它是   CanUserAddRows属性设置为true   并且用户开始输入   底部的空行,DataGrid   将使用您的默认构造函数   ItemViewModel创建一个松散的项目   然后将它添加到   采集。没有任何迹象   来自DG,它想要添加一个项目   collection.c   我什么都想不到   其他控制很复杂   足以能够添加项目   收集本身。
  相反的   场景是你有ListView   绑定到您的集合和命令   表示用户的意图   添加新项 - 然后在命令处理程序中   您只需将新项添加到DataModel   让单向同步完成其余部分   工作。在这种情况下,ListView不是   能够添加到它的集合   呈现。

关于同步过程本身,请查看Bindable LINQ项目 - 它可以最小化代码量并提高可读性。例如,Tom发布的代码将转换为以下内容:

class ViewModel
{
  public Model Model;
  public ObservableCollection<FooView> Foos;
  public ViewModel()
  {
    Foos = from foo in Model.Foos.AsBindable()
           select new FooView(foo);
  }
}
  

编辑2:使用B-LINQ一段时间之后我应该说你可能遇到性能问题。我用它来同步相对较大的集合(数百个元素)集合,每秒添加和删除数十个元素,我不得不放弃并按照Tom建议的方式实现同​​步。
  我仍然使用B-LINQ,但是在项目的那些部分中,集合很小并且性能不是问题。

答案 2 :(得分:3)

我也在努力与两个集合的双向同步,以便通过MVVM与WPF一起使用。我在这个问题上写了MVVM: To Wrap or Not to Wrap? How much should the ViewModel wrap the Model? (Part 1)MVVM: To Wrap or Not to Wrap? Should ViewModels wrap collections too? (Part 2),其中包括一些显示双向同步的示例代码。但是,如帖子所述,实施情况并不理想。我认为它可以作为概念证明。

我喜欢Alex_P发布的BLINQCLINQObtics框架。这是一个非常好的方式来获得同步behvaior的一面。也许另一方(从VM到Model)可以通过备用路径实现?我刚刚在我的博客上发布了part 3,讨论了其中的一些内容。

从我所看到的情况来看,在LINQ语句将数据投影到新结构的情况下,不支持双向通过BLINQ和CLINQ。

但是,在LINQ查询返回与底层集合相同的数据类型的情况下,CLINQ可能支持双向同步。这更像是一个过滤场景,它与包装模型中数据的ViewModel的用例不匹配。

答案 3 :(得分:1)

我已经编写了一些帮助程序类,用于在View模型对应here中包装可观察的业务对象集合,也许它应该扩展为另一种方式。一直在寻找贡献...

答案 4 :(得分:1)

我提出了一个基于MVVM的通用撤销/重做框架,它使用了与您描述的问题相关的一些技术。它使用实现此接口的集合:

public interface MirrorCollectionConversor<V, D>
{
   V GetViewItem(D modelItem, int index);
   D GetModelItem(V viewItem, int index);
}

(V代表ViewModel项目,D代表模型项目)

使用此界面,它会在模型​​集合更改时自动同步viewmodel集合。如果更改viewmodel,则只需将更改重定向到模型集合。

GetViewItem函数为viewmodel对象与其模型对应物的相关性提供了一些灵活性。

您可以找到详细信息here

(我承认构造非常复杂,我很乐意听取建议)。

答案 5 :(得分:0)

使用我的ObservableComputations库,您可以创建ObservableCollection,它是对另一个ObservableCollection进行计算并与之同步的结果。您在源集合上进行标准操作(添加,删除):计算出的Collection会反映所有更改:

Filtering<Order> expensiveOrders = orders.Filtering(o => o.Price > 25); 

expensiveOrders.CollectionChanged += (sender, eventArgs) =>
{
     // see the changes (add, remove, replace, move, reset) here
};

// Start the changing...
orders.Add(new Order(8, 30));
orders.RemoveAt(1);
orders[0].Price = 60;
orders[4].Price = 10;
orders.Move(5, 1);
orders[1] = new Order(10, 17);