【问题标题】:Two-way bind a "virtual" list of strings to a column双向将字符串的“虚拟”列表绑定到列
【发布时间】:2012-03-31 10:14:00
【问题描述】:

我有一个字符串列表。

嗯,从概念上讲。它们存储在其他地方,但我想提供一个像列表一样的对象(并在此之上提供任何必要的事件),以及我可以绑定到的属性。

我想对该数据建立双向绑定,将其显示为DataGrid 中的可修改列。我有以下问题:

  • 我不能进行双向绑定,因为绑定需要一个路径(即我不能让它看起来像列中的{Binding}{Binding Path=.},必须是{Binding Path=someField"} 才能修改,如果我猜对了,这听起来很合理)。
  • 我不太清楚代理集合对象在接口方面应该是什么样子(IEnumerable + INotifyCollectionChanged 足够吗?)

是否有任何解决方案不涉及为集合中的每个字符串创建一个代理对象?你能推荐一个高效的设计吗?


为了让讨论保持正常,假设我想绑定到这样的东西:

class Source {
    public String getRow(int n);
    public void setRow(int n, String s);
    public int getCount();
    public void addRow(int position, String s);
    public void removeRow(int position);
}

这不完全是我的情况,但是当我知道如何绑定到这个时,我想我将能够处理这样的任何情况。

我可以在该 Source 之上提供一个适配器对象,以及任何必要的接口和事件,但我不希望每行数据有一个适配器对象。

【问题讨论】:

  • 你绑定的是什么类型的控件?
  • 你绑定的控件是什么?默认模式取决于此。编辑:Opps 已经有人问过了 :)
  • 我已经提到它是DataGrid 中的一列;-)

标签: c# .net wpf data-binding binding


【解决方案1】:

虽然为 Source 制作适配器相对清晰,但不幸的是,第二个问题的核心('不将每个字符串都包装在 miniobject 中')是 .Net 和 WPF 中内置的冲突。

首先,WPF 确实为您提供了许多注册“修改数据”回调的方法,但没有提供注册可提供值的回调的方法。我的意思是,“设置”阶段只能扩展,不可拦截,而“获取”阶段 - 什么都没有。 WPF 将简单地保留并返回它曾经缓存过的任何数据。

第二件事是,在 .Net 中,string 是...不可变的。

现在,如果您直接将字符串作为无路径绑定或作为数据上下文提供给任何控件,那么您将搞砸。问题是,WPF 实际上只传递绑定的实际值,而没有“它来自哪里”的信息。底层控件将被简单地赋予字符串实例,并且由于字符串无法自​​行更改,因此无法正常修改它。您甚至不会收到有关此类尝试的通知,就像使用只读属性一样。更重要的是 - 如果您设法拦截这样的修改尝试,并且如果您生成了正确的新字符串,则 WPF 将永远不会再次要求您提供新值。要更新 UI,您必须手动强制 WPF 重新询问您,例如更改原始绑定,使其指向其他位置(指向新值)或设置数据上下文(指向新实例)。它可以通过一些 VisualTree 扫描来实现,因为每个“更改”回调都会为您提供 DependencyObject(控件!),因此您可以向上/向下扫描并篡改它们的属性。记住该选项 - 我'待会儿会提到这个。

所以,一切都归结为这样一个事实,即要获得一个正常的 2 路绑定,您不必有一个路径,您“只”必须有一个可变的底层数据对象。如果您有不可变的 - 那么您必须使用绑定到包含不可变值的可变属性..

话虽如此,如果你想修改字符串,你只需要将字符串包装起来。

另一个问题是,如何做到这一点。有很多方法可以做到这一点。当然,您可以像 Joe 和 Davio 建议的那样简单地包装它们(Joe 注意:那里也需要 INotify),或者您可以尝试使用附加属性和/或行为和/或转换器执行一些 XAML 技巧来做到这一点你。这是完全可行的,例如my other post - 我已经展示了如何“注入一个虚拟属性”,从其他地方完全提取数据(一个绑定+转换器即时执行包装,第二个绑定从附加包装)。这样您就可以在字符串上创建一个“Contents”属性,并且该属性可以简单地返回字符串本身,并且它完全可以双向绑定,没有例外。

但是..它不会以两种方式工作。

在绑定/行为/转换器链的根部某处,会有不可变字符串。一旦您的智能自动包装绑定链通过“修改后”回调触发,您将收到一对旧/新值的通知。您将能够将值重新映射到新旧字符串。如果您完美地实现了一切,WPF 将简单地使用新值。如果您在某个地方绊倒,那么您将不得不人为地将新值推回 UI(请参阅我要求您记住的选项)。所以,没关系。没有包装器,旧值可见,它是可变的,你有新值,UI 显示新值。存储呢?

与此同时,您获得了一个旧/新值对。如果你分析它们,你会得到旧的/新的字符串。但是如何更新旧的不可变字符串?做不到。即使自动换行有效,即使 UI 有效,即使编辑似乎有效,您现在正面临着真正的任务:您的 onmodified 回调已被调用,您必须实际更新该不可变字符串片段。

首先,您需要您的来源。是静态的吗?呸。多么幸运!所以肯定是实例化的。在 on-modified 回调中,我们只得到一个 old+new 字符串。如何获取 Source 实例?选项:

  • 扫描 VisualTree 并在数据上下文中搜索它并使用找到的任何内容..
  • 添加更多附加属性和绑定以将虚拟“源”属性绑定到每个字符串并从新值中读取该属性

可行,但有异味,但没有其他选择。

等等,还有更多:不仅需要旧/新值和 Source 实例!您还需要 ROW INDEX。哦!如何从绑定数据中获取?再次,选项:

  • 扫描 VisualTree 并搜索它 (blaargh)...
  • 添加更多附加属性和绑定以将虚拟“RowIndex”属性绑定到每个 (blaaergh)...

目前,虽然我看到所有这些似乎都可以实现并且实际上可能工作正常,但我真的认为将每个字符串包装在一个小

public class LocalItem // + INotifyPropertyChanged
{
    public int Index { get; }
    public Source Source { get; }

    public string Content
    {
        get { Source...}
        set { Source... }
    }
}

将变得更易读、更优雅,而且实现起来更短。而且更不容易出错,因为更多细节将是明确的,而不是一些 WPF 的绑定+附加魔法..

【讨论】:

    【解决方案2】:

    我觉得你的方法有点奇怪。

    DataGrids 通常用于显示行。行由属于一起的数据组成。 例如,您可以轻松地将一行映射到某个类。这意味着数据网格中的列代表类中的属性。

    您尝试做的恰恰相反,您尝试获取列值而不是行值之间的关系。

    拥有一个类的集合,然后将列绑定到该集合不是更容易吗?

    例如

    class MyClass : INotifyPropertyChanged
    {
        // Remember to actually implement INotifyPropertyChanged
        string Column;
    }
    

    如果您有 MyClass 的 ObservableCollection,您可以将 DataGrid 绑定到此集合。每当我称为“列”的属性发生变化时,您都可以更新您的特殊列表。

    您可以通过连接一些事件来做到这一点。随着 INotifyPropertyChanged 的​​实现,如果您直接更新“列”值,您的列将被更新。

    【讨论】:

      【解决方案3】:

      我有这段代码用于将自定义对象列表绑定到 DataContextMenu。您可以更改它以使用字符串列表并将其绑定到您需要的内容

      class SampleCode
      {
          class Team
          {
              private string _TeamName = "";
              private int _TeamProperty1 = 0;
              ObservableCollection<Territory> _Territories = new ObservableCollection<Territory>();
      
              public Team(string tName)
              {
                  this.TeamName = tName;
              }
      
              public ObservableCollection<Territory> Territories
              {
                  get { return _Territories; }
                  set { _Territories = value; }
              }
      
              public string TeamName
              {
                  get { return _TeamName; }
                  set { _TeamName = value; }
              }
      
              public int TeamProperty1
              {
                  get { return _TeamProperty1; }
                  set { _TeamProperty1 = value; }
              }
      
          }
      
          class Territory
          {
              private string _TerritoryName = "";
              Team _AssociatedTeam = null;
      
              public Territory(string tName, Team team)
              {
                  this.TerritoryName = tName;
                  this.AssociatedTeam = team;
              }
      
              public Team AssociatedTeam
              {
                  get { return _AssociatedTeam; }
                  set { _AssociatedTeam = value; }
              }
      
              public string TerritoryName
              {
                  get { return _TerritoryName; }
                  set { _TerritoryName = value; }
              }
      
      
              public void Method1()
              {
                  //Do Some Work
              }
          }
      
          class MyApplication
          {
              ObservableCollection<Team> _Teams = new ObservableCollection<Team>();
              ContextMenu _TeritorySwitcher = new ContextMenu();
              public MyApplication()
              {
      
              }
      
              public void AddTeam()
              {
                  _Teams.Add(new Team("1"));
                  _Teams.Add(new Team("2"));
                  _Teams.Add(new Team("3"));
                  _Teams.Add(new Team("4"));
      
                  foreach (Team t in _Teams)
                  {
                      t.Territories.Add(new Territory("1", t));
                      t.Territories.Add(new Territory("2", t));
                      t.Territories.Add(new Territory("3", t));
                  }
      
                  SetContextMenu();
              }
      
              private void SetContextMenu()
              {
                  HierarchicalDataTemplate _hdtTerritories = new HierarchicalDataTemplate();
                  _hdtTerritories.DataType = typeof(Territory);
      
                  HierarchicalDataTemplate _hdtTeams = new HierarchicalDataTemplate();
                  _hdtTeams.DataType = typeof(Team);
      
                  FrameworkElementFactory _TeamFactory = new FrameworkElementFactory(typeof(TreeViewItem));
                  _TeamFactory.Name = "txtTeamInfo";
                  _TeamFactory.SetBinding(TreeViewItem.HeaderProperty, new Binding("TeamProperty1"));
      
                  FrameworkElementFactory _TerritoryFactory = new FrameworkElementFactory(typeof(TreeViewItem));
                  _TerritoryFactory.Name = "txtTerritoryInfo";
                  _TerritoryFactory.SetBinding(TreeViewItem.HeaderProperty, new Binding("TerritoryProperty1"));
      
      
                  _hdtTeams.ItemsSource = new Binding("Territories");
      
                  _hdtTeams.VisualTree = _TeamFactory;
                  _hdtTerritories.VisualTree = _TerritoryFactory;
      
                  _hdtTeams.ItemTemplate = _hdtTerritories;
      
                  _TeritorySwitcher.ItemTemplate = _hdtTeams;
                  _TeritorySwitcher.ItemsSource = this._Teams;
              }
          }
      }
      

      【讨论】:

        【解决方案4】:

        懒惰的解决方案

        ObservableCollection&lt;string&gt; 派生并让该集合从源中填充。在派生类中,注册到集合更改事件并相应地更新源。将 DataGrid 列绑定到可观察集合。

        这写起来应该很简单,但有一个很大的缺点是要复制集合中的所有数据。

        更高效的解决方案

        创建一个适配器(按照您的建议)并实现IList&lt;string&gt;INotifyCollectionChanged。让列表操作直接落到源头。将 DataGrid 列绑定到适配器。

        这种方法需要一些乏味的样板文件,但它是 WPF 控件和您的 Source 之间的一个薄层。

        【讨论】:

          【解决方案5】:

          这实际上取决于您如何实现 UI。 Bea Stollnitz 在http://bea.stollnitz.com/blog/?p=344 上为 WPF DataGrid 虚拟化 ItemsSource 做了一篇出色的文章。在工作中,我用它来编辑和显示数据。

          【讨论】:

            【解决方案6】:

            最简单的方法是将字符串放在包装类中。

            public class Wrapper
            {
                public string Content{get;set;}
            }
            

            然后你通过包装类使用字符串。这是列表项保持不变,但内容发生了变化。 问题是,如果您不这样做,则会删除旧字符串并创建新字符串,并且会混淆集合。

            【讨论】:

              【解决方案7】:

              ObservableCollection&lt;string&gt; 开头。然后将可绑定控件的ItemsSource 设置为 ObservableCollection。

              【讨论】:

              • 酷;但是如何修改 ObservableCollection 以检索和更新我的源中的数据?
              猜你喜欢
              • 2019-12-24
              • 1970-01-01
              • 2023-04-01
              • 1970-01-01
              • 2011-12-18
              • 1970-01-01
              • 2015-06-14
              • 2017-02-04
              • 1970-01-01
              相关资源
              最近更新 更多