简体   繁体   中英

How to bind ComboBox properly in WPF?

I'm new to WPF and MVVM and I'm developing a test WPF application following the MVVM design pattern. My database has 2 entities, Cards and Departments. Any card can have only 1 department, so it's a one-to-many relationship.

I've created the following ViewModel in order to bind to the view:

public class CardViewModel : INotifyPropertyChanged
{
    public CardViewModel(Card card)
    {
        this.Card = card;

        SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder();
        builder.DataSource = ".\\SQLExpress";
        builder.InitialCatalog = "TESTDB";
        builder.IntegratedSecurity = true;

        SybaseDatabaseContext myDB = new SybaseDatabaseContext(builder.ConnectionString);

        var query = from d in myDB.Departments
                    select d;

        this.Departments = new ObservableCollection<Department>(query);
    }
    private Card _Card;
    private ObservableCollection<Department> _Departments;

    public Card Card
    {
        get { return _Card; }
        set
        {
            if (value != this._Card)
            {
                this._Card = value;
                SendPropertyChanged("Card");
            }
        }
    }

    public ObservableCollection<Department> Departments
    {
        get { return _Departments; }
        set
        {
            this._Departments = value;
            SendPropertyChanged("Departments");
        }
    }

    #region INPC
    // Logic for INotify interfaces that nootify WPF when change happens
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void SendPropertyChanged(String propertyName)
    {
        if ((this.PropertyChanged != null))
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion
}

The CardForms' datacontext is currently being set to an instance of the CardViewModel in the code where the CardForm is being instantiated, but I'm going to create a IoC container or dependency injections down the line.

Everything binds correctly except for the ComboBox that should contain all departments and that has the current department in the Card instance selected (card.Department). Here's the XAML for the ComboBox:

<ComboBox Height="23" HorizontalAlignment="Left" Margin="350,64,0,0" 
          Name="comboBoxDepartment" VerticalAlignment="Top" Width="120"
          IsSynchronizedWithCurrentItem="True"
          ItemsSource="{Binding Path=Departments}" 
          DisplayMemberPath="DepartmentName"
          SelectedItem="{Binding Path=Card.Department, Mode=TwoWay}" />

The departments are displayed in the combobox, but the current department of the card isn't and if I try to change it I get and error saying "Cannot add an entity with a key that is already in use".

So, my question is, how do I bind this combobox correctly to my ViewModel?

PS I know populating the ObservableCollection<Department> in the ViewModel is probably not the right way to do it, but I could not think of a better way at the time. If you have any suggestions for this also, please let me know.

Additionally, this is the Card model:

[Table(Name = "Card")]
public class Card : INotifyPropertyChanged, INotifyPropertyChanging
{
    private string _CardID;
    private string _Holder;
    private Int16? _DepartmentNo;

    [Column(UpdateCheck = UpdateCheck.WhenChanged)]
    public string CardID
    {
        get
        {
            return this._CardID;
        }
        set
        {
            if (value != this._CardID)
            {
                SendPropertyChanging();
                this._CardID = value;
                SendPropertyChanged("CardID");
            }
        }
    }

    [Column(UpdateCheck = UpdateCheck.WhenChanged)]
    public string Holder
    {
        get
        {
            return this._Holder;
        }
        set
        {
            if (value != this._Holder)
            {
                SendPropertyChanging();
                this._Holder = value;
                SendPropertyChanged("Holder");
            }
        }
    }

    [Column(CanBeNull = true, UpdateCheck = UpdateCheck.WhenChanged)]
    public Int16? DepartmentNo
    {
        get
        {
            return this._DepartmentNo;
        }
        set
        {
            if (value != this._DepartmentNo)
            {
                SendPropertyChanging();
                this._DepartmentNo = value;
                SendPropertyChanged("DepartmentNo");
            }
        }
    }

    private EntityRef<Department> department;
    [Association(Storage = "department", ThisKey = "DepartmentNo", OtherKey = "DepartmentNo", IsForeignKey = true)]
    public Department Department
    {
        get
        {
            return this.department.Entity;
        }
        set
        {
            Department previousValue = this.department.Entity;
            if (((previousValue != value)
                        || (this.department.HasLoadedOrAssignedValue == false)))
            {
                this.SendPropertyChanging();
                if ((previousValue != null))
                {
                    this.department.Entity = null;
                    previousValue.Cards.Remove(this);
                }
                this.department.Entity = value;
                if ((value != null))
                {
                    value.Cards.Add(this);
                    this._DepartmentNo = value.DepartmentNo;
                }
                else
                {
                    this._DepartmentNo = default(Nullable<short>);
                }
                this.SendPropertyChanged("Department");
            }
        }
    }

I edited the constructor in the CardViewModel to take the DataContext as a parameter and that did it. This is the new CardViewModel constructor:

public CardViewModel(Card card, SybaseDatabaseContext myDB)
{
    this.Card = card;

    var query = from d in myDB.Departments
                select d;

    this.Departments = new ObservableCollection<Department>(query);
}

Had to do a bit of research on this myself. Thought I would contribute with a self answered question, but found this open current question...

The ComboBox is designed to be a kind of textbox that restricts it's possible values to the contents of a given list. The list is provided by the ItemsSource attribute. The current value of the ComboBox is the SelectedValue property. Typically these attributes are bound to relevant properties of a corresponding ViewModel.

The following example shows wired ComboBox together with a TextBox control used to redundantly view the current value of the ComboBox by sharing a view model property. (It is interesting to note that when TextBox changes the shared property to a value outside the scope of the ComboBox 's list of values, the ComboBox displays nothing.)

Note: the following WPF/C# example does does use code-behind and so presents the ViewModel as merely the datacontext of the view and not a partial class of it, a current implementation constraint when using WPF with F#.

WPF XAML

<Window 
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:m="clr-namespace:WpfApplication1"
  Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <m:MainWindowVM />
  </Window.DataContext>
  <StackPanel>
    <TextBox Text="{Binding SelectedString}" />
    <ComboBox ItemsSource="{Binding MyList}" SelectedValue="{Binding SelectedString}" />
  </StackPanel>
</Window>

C# ViewModel

using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication1
{
  public class MainWindowVM : INotifyPropertyChanged
  {
    string selectedString;
    void NotifyPropertyChanged(string propertyName)
    {
      if (PropertyChanged == null) return;
      PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
    public string SelectedString
    {
      get { return selectedString; }
      set
      {
        selectedString = value;
        NotifyPropertyChanged("SelectedString");
      }
    }
    public List<string> MyList
    {
      get { return new List<string> { "The", "Quick", "Brown", "Fox" }; }
    }
    public event PropertyChangedEventHandler PropertyChanged;
  }
}

By default, ToString() is used to interpret the objects in the list. However, ComboBox offers DisplayMemberPath and SelectedValuePath attributes for specifying paths to specific object properties for corresponding displayed and stored values. These paths are relative to the list object element so a path of "Name" refers to Name on a list object item.

The "Remarks" section of this MSDN link explains the interpretations of the IsEditable and IsReadOnly ComboBox properties.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM