【问题标题】:MVVM: ViewModels for bidirectional model elementsMVVM:双向模型元素的视图模型
【发布时间】:2016-07-30 23:17:18
【问题描述】:

假设我有以下模型结构:

class Team {
    public string Name {get;set; }
    public List<Player> players {get;set;}
}

class Player {
    public int Age {get;set;}
    public string Name {get;set;}
    public Team Team {get;set;}
}

我希望为此模型创建视图模型。但是,我也想避免在 TeamVM 中复制 Player 的所有属性,反之亦然(对于这个简单的示例,这是可行的,但实际上相当麻烦)。

查看文献和在线文章,似乎“纯粹”的方式是为每个模型创建一个 ViewModel,并且让一个 ViewModel 只返回其他 ViewModel 而从不返回 Model。这一切都很好,但我的问题是:如何在不陷入递归陷阱的情况下创建这些视图模型。假设我这样做:

public class TeamVM: ViewModel<Team> {

   private ObservableCollection<PlayerVM> _players;

   public TeamVM(Team t): base(t) {
       _players = new ObservableCollection();
       foreach (Player p in t.players) {
          _players.Add(new PlayerVM(t));
       }
   }

   public string Name {
      get { return _modelElement.Name; }
      set { _modelElement.Name = value; NotifyPropertyChanged(); }
   }

   public ObservableCollection<PlayerVM> Players {
      get { return _players; }
   }
}

public class PlayerVM : ViewModel<Player> {

   private TeamVM _teamVM;

   public PlayerVM(Player p): base(p) {
       _teamVm = new TeamVM(p.Team);
   }

   public int Age {
      get { return _modelElement.Age; }
      set { _modelElement.Age = value; NotifyPropertyChanged(); }
   }
   public string Name {
      get { return _modelElement.Name; }
      set { _modelElement.Name = value; NotifyPropertyChanged(); }
   }
   public TeamVM Team {
      get { return _teamVM; }
      set { _teamVm = value; NotifyPropertyChanged(); }
   }
}

显然,上述方法永远不会起作用,因为它会创建递归:创建 TeamVM 会导致创建 PlayerVM,而 PlayerVM 又会再次生成 TeamVM,等等。

现在,我通过添加如下中间类解决了这个问题:

public class TeamMinimalVM: ViewModel<Team> {

   public TeamVM(Team t): base(t) {
   }

   public string Name {
      get { return _modelElement.Name; }
      set { _modelElement.Name = value; NotifyPropertyChanged(); }
   }
}

public class TeamVM: TeamMinimalVM {

   private ObservableCollection<PlayerVM> _players;

   public TeamVM(Team t): base(t) {
       _players = new ObservableCollection();
       foreach (Player p in t.players) {
          _players.Add(new PlayerVM(t));
       }
   }
}

然后让 PlayerVM 依赖于 TeamMinimalVM 而不是 TeamVM。这意味着在视图中,您可以执行以下操作:{Binding Player.Team.Name} 但不能执行 {Binding Player.Team.Players.Name},我想这对我来说还可以,因为我不认为无论如何,这样做是个好主意。

我现在的问题是:是否有更好/更“标准”的方式来执行双向模型元素的“纯”虚拟机?我不想将一种类型的属性克隆到另一种类型中(太多了),也不想直接公开 Model 元素。

最后,我使用的 ViewModel 类就是这个(只是为了完整性,但它对我认为的问题并不重要。)

public class ModelElementViewModel<T> : ObservableObject where T : class 
{
    private bool _modelElementChanged;
    private T _modelElement;

    public ModelElementViewModel(T element)
    {
        _modelElement = element;
    }

    /// <summary>
    /// The underlying model element for this viewmodel. Protected as one should not bind directly to model elements from the gui.
    /// </summary>
    internal T ModelElement {
        get { return _modelElement; }
        set {
            if (_modelElement != value)
            {
                _modelElement = value;
                ModelElementChanged = false;
                NotifyAllPropertiesChanged();
            }
            ; }
    }

    /// <summary>
    /// Property that can be used to see if the underlying modelelement was changed through this viewmodel (note that an external 
    /// change to the model element is not tracked!)
    /// </summary>
    public bool ModelElementChanged {
        private set
        {
            if (_modelElementChanged != value)
            {
                _modelElementChanged = value;
                NotifyPropertyChanged();
            }
        }
        get
        {
            return _modelElementChanged;
        }
    }

    protected override void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
    {
        ModelElementChanged = true;
        base.NotifyPropertyChanged(propertyName);
    }

}

编辑:

我最初的问题并不清楚的是,球员并不是由球队专门使用的。我希望以下三种情况起作用:

  1. 我希望能够为单个玩家创建一个显示所有玩家信息的视图
  2. 我希望能够为球队创建一个视图,显示该球队的信息以及所有球员及其统计数据的表格
  3. 例如,我还希望能够拥有一个 Playersbook 视图,该视图由一个表格组成,其中显示所有已知球员及其团队名称。

【问题讨论】:

  • “看文献和在线文章,似乎“纯粹”的方式是为每个模型创建一个 ViewModel” - 这是不正确的。您为每个视图创建 ViewModel,而不是为每个模型。例如,VM 可能负责聚合许多模型。 VM 的工作是向视图提供信息
  • @MickyD:我同意您评论的基本思想,但仍然存在如何表示“玩家”数据的问题。可能有代表团队的视图,也有代表单个玩家的视图(例如用于ItemsControl 或其他东西的模板)。您认为播放器视图及其视图模型是什么样的?
  • 恐怕我不太明白这个问题。我的意思是,到目前为止提出的答案暗示了我认为显而易见的解决方案——只需将TeamVM 引用传递给PlayerVM 构造函数,而不是让PlayerVM 对象创建一个新对象(可选地比较TeamVM.Team Player.Team 值的属性,以确保关系有效)——但由于它看起来很明显,我不清楚你为什么不这样做。还有一个问题是为什么PlayerVM 对象仍然需要TeamVM 对象。那有什么意义呢?
  • @MickyD 我同意你的说法。我创建虚拟机的起点是我需要的视图。然而,在那个特定的句子中,我的意思是在构建 VM 时,“纯粹主义者”似乎说你永远不应该返回模型元素,因此,应该为每个使用的域对象创建一个 VM 包装器。
  • @PeterDuniho 我之所以不那样做,是因为在实际项目中,并没有这么明确的“所有权”。为了扩展这里的类比,假设有其他模型元素(具有相应的视图模型)也引用了播放器(尽管播放器没有引用返回),例如列出所有现有播放器的“PlayersBook”。创建 PlayersBookVM 时,您希望创建 PlayerVM,但您无权访问 TeamVM。

标签: c# wpf mvvm


【解决方案1】:

您的班级有一个明确的层次结构:团队聚集了球员。球队是所有者,球员是所有者。因此,在创建玩家虚拟机时,您可以将团队虚拟机作为构造函数参数传递。

这样做的明显限制是现在你不能有没有球队的球员。可能的解决方案是:强制玩家始终由某个团队拥有;支持 null 作为团队 VM 并在以后设置适当的值;创建一个“空团队”对象并将其用于无团队玩家。

在这种情况下,如果聚合层次结构清晰,我会使用我的OwnedObservableCollection&lt;T, TOwner&gt;。有了它,我可以在团队中创建一个集合_players = new OwnedObservableCollection&lt;PlayerVM, TeamVM&gt;(this),然后只需使用AddRemove 在团队中添加和删除玩家。

【讨论】:

  • 参考我上面的评论,虽然确实“每个玩家”都有一个团队,但视图模型有时是从一个玩家开始的。假设您有一本列出比赛中所有玩家的玩家手册。当你创建一个 playerbookvm 时,你想创建 PlayerVMs 而不能访问 teamvm。理论上,您可以先创建 teamvm,但这可能并不总是需要。
猜你喜欢
  • 2013-03-19
  • 1970-01-01
  • 1970-01-01
  • 2010-12-07
  • 1970-01-01
  • 1970-01-01
  • 2013-05-27
  • 1970-01-01
  • 2019-10-03
相关资源
最近更新 更多