【问题标题】:How to maintain ComboBox.SelectedItem reference when DataSource is resorted?使用 DataSource 时如何维护 ComboBox.SelectedItem 引用?
【发布时间】:2010-03-12 16:58:10
【问题描述】:

这对我来说确实是一个错误,但也许一些数据绑定专家可以启发我? (我的 WinForms 数据绑定知识非常有限。)

我有一个ComboBox 绑定到一个已排序的DataView。当DataView 中项目的属性发生更改以使项目被重新使用时,我的ComboBox 中的SelectedItem 不会保持同步。它似乎指向某个完全随机的地方。这是一个错误,还是我在数据绑定中遗漏了什么?

这是一个重现问题的示例应用程序。您只需要一个Button 和一个ComboBox

public partial class Form1 : Form
{
    private DataTable myData;

    public Form1()
    {
        this.InitializeComponent();

        this.myData = new DataTable();
        this.myData.Columns.Add("ID", typeof(int));
        this.myData.Columns.Add("Name", typeof(string));
        this.myData.Columns.Add("LastModified", typeof(DateTime));
        this.myData.Rows.Add(1, "first", DateTime.Now.AddMinutes(-2));
        this.myData.Rows.Add(2, "second", DateTime.Now.AddMinutes(-1));
        this.myData.Rows.Add(3, "third", DateTime.Now);

        this.myData.DefaultView.Sort = "LastModified DESC";
        this.comboBox1.DataSource = this.myData.DefaultView;
        this.comboBox1.ValueMember = "ID"; 
        this.comboBox1.DisplayMember = "Name";
    }

    private void saveStuffButton_Click(object sender, EventArgs e)
    {
        DataRowView preUpdateSelectedItem = (DataRowView)this.comboBox1.SelectedItem;
        // OUTPUT: SelectedIndex = 0; SelectedItem.Name = third
        Debug.WriteLine(string.Format("SelectedIndex = {0:N0}; SelectedItem.Name = {1}", this.comboBox1.SelectedIndex, preUpdateSelectedItem["Name"]));

        this.myData.Rows[0]["LastModified"] = DateTime.Now;

        DataRowView postUpdateSelectedItem = (DataRowView)this.comboBox1.SelectedItem;
        // OUTPUT: SelectedIndex = 2; SelectedItem.Name = second
        Debug.WriteLine(string.Format("SelectedIndex = {0:N0}; SelectedItem.Name = {1}", this.comboBox1.SelectedIndex, postUpdateSelectedItem["Name"]));

        // FAIL!
        Debug.Assert(object.ReferenceEquals(preUpdateSelectedItem, postUpdateSelectedItem));
    }
}

澄清一下:

  • 我知道我将如何修复上面的简单应用程序——我只是将其包括在内以演示问题。我关心的是,当底层数据行的更新可能发生在任何地方(可能是在另一种形式上)时,如何解决它。
  • 我真的很想接收对我的数据源的更新、插入、删除等。我尝试只绑定到与DataTable 分离的DataRows 数组,但这会引起额外的麻烦。

【问题讨论】:

    标签: winforms data-binding combobox


    【解决方案1】:

    只需将 BindingContext 添加到 ComboBox :

    this.comboBox1.DataSource = this.myData.DefaultView;  
    this.comboBox1.BindingContext = new BindingContext();  
    this.comboBox1.ValueMember = "ID";  
    this.comboBox1.DisplayMember = "Name";
    

    顺便说一句,尽量不要为您的小部件(comboBox1,...)保留自动生成的名称,这很脏。 :-P

    【讨论】:

      【解决方案2】:

      我目前看到的唯一有希望的解决方案是将组合框绑定到分离的数据源,然后在每次“真实”DataView 更改时更新它。这是我到目前为止所拥有的。似乎可以正常工作,但是 (1) 它完全是 hack,并且 (2) 它根本无法很好地扩展。

      在表单声明中:

      private DataView shadowView;
      

      在表单初始化中:

      this.comboBox1.DisplayMember = "Value";
      this.comboBox1.ValueMember = "Key";
      this.shadowView = new DataView(GlobalData.TheGlobalTable, null, "LastModified DESC", DataViewRowState.CurrentRows);
      this.shadowView.ListChanged += new ListChangedEventHandler(shadowView_ListChanged);
      this.ResetComboBoxDataSource(null);
      

      然后是破解:

      private void shadowView_ListChanged(object sender, ListChangedEventArgs e)
      {
          this.ResetComboBoxDataSource((int)this.comboBox1.SelectedValue);
      }
      
      private void ResetComboBoxDataSource(int? selectedId)
      {
          int selectedIndex = 0;
          var detached = new KeyValuePair<int, string>[this.shadowView.Count];
          for (int i = 0; i < this.shadowView.Count; i++)
          {
              int id = (int)this.shadowView[i]["ID"];
              detached[i] = new KeyValuePair<int, string>(id, (string)this.shadowView[i]["Name"]);
              if (id == selectedId)
              {
                  selectedIndex = i;
              }
          }
          this.comboBox1.DataSource = detached;
          this.comboBox1.SelectedIndex = selectedIndex;
      }
      

      必须在 Dispose 中分离事件处理程序:

      this.shadowView.ListChanged -= new ListChangedEventHandler(shadowView_ListChanged);
      

      【讨论】:

        【解决方案3】:

        您的示例对它更新的列上的数据进行排序。当更新发生时,行的顺序会改变。组合框使用索引来跟踪它的选定项目,因此当对项目进行排序时,索引指向不同的行。您需要在更新行之前捕获 comboxBox1.SelectedItem 的值,并在更新完成后将其重新设置:

                DataRowView selected = (DataRowView)this.comboBox1.SelectedItem;
                this.myData.Rows[0]["LastModified"] = DateTime.Now;
                this.comboBox1.SelectedItem = selected;
        

        【讨论】:

        • @Rory:谢谢;我想我不是很清楚。我理解您回答中的所有内容,但是-也许只是我-这似乎不是一个错误吗?在我原来的问题中的人为示例中,修复(如您指定的)很容易。但在实际应用中,属性的更改可能发生在任何地方,很可能以完全不同的形式发生。必须处理 INotifyPropertyChanged 之类的东西似乎是一种黑客行为,只是为了防止 ComboBox 根据数据源的排序算法指向随机项目。这似乎会使 ComboBox 中的绑定变得非常脆弱。
        • @Dave:winforms 中的数据绑定对我来说一直感觉有点像一个错误,这是一个很好的概念,但实现非常糟糕。谢天谢地,WPF 和 Silverlight 在这方面要好得多,但我想如果你被困在使用 winforms 的话,那也不是很舒服。不幸的是,我认为解决您的问题的最佳方法是完全避免这种情况:不要更新排序字段并避免添加可能在现有行之前插入的行。抱歉,我知道这没有多大帮助,但我想不出其他方法。
        【解决方案4】:

        从架构的角度来看,在重新绑定 DataSource 时必须清除 SelectedItem,因为 DataBinder 不知道您的 SelectedItem 是否会持续存在。

        从功能的角度来看,DataBinder 可能无法确保旧 DataSource 中的 SelectedItem 在新 DataSource 中是相同的(它可以是具有相同 SelectedItem ID 的不同 DataSource)。

        它更多的是应用程序功能或自定义控件功能,而不是通用数据绑定过程。

        恕我直言,如果您想保持 SelectedItem 重新绑定,您有这些选择:

        • 创建一个具有持久性选项的可重用自定义控件/自定义 DataBinder,该选项尝试使用您的所有数据验证设置 SelectedItem(使用 DataSource/项目标识来确保项目有效性)

        • 使用表单/应用程序上下文(如 ASP.NET 的 ViewState)专门将其保留在您的表单上。

        .NET 市场上的一些控件通过重新绑定(包括选择)来自它们自己的持久数据源的控件来帮助您如果 DataSource 未更改且 DataBind 未调用。这是最好的做法。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2013-01-20
          • 2015-10-20
          • 2016-02-09
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-03-06
          相关资源
          最近更新 更多