So I have a custom class for my object like this:
class ClassA
{
public ClassA(string name)
{
this.AName = name;
}
public string AName { get; set; }
}
Then, I have a list of these in my ViewModel:
public List<ClassA> ObjectList
{
get { return _myobjects; }
set
{
_myobjects = value;
NotifyPropertyChanged("ObjectList");
}
}
Now, I have a combobox in my window with this list as its Itemssource:
<ComboBox Width="150" x:Name="cboObjectList"
ItemsSource="{Binding Path=ObjectList}" DisplayMemberPath="AName"/>
The problem is that when I select any item in the combobox, The SelectedIndex property is always = -1 and also the SelectedItem is null.
What am I doing wrong?
Found out the cause of the cause of the problem. There was an old line of code where I programmatically set the SelectedIndex to -1 :)
I have a ComboBox defined like this:
<ComboBox
ItemsSource="{Binding Choices}"
SelectedItem="{Binding Value}"
Text="{Binding Text}"
IsEditable="True"
TextSearch.TextPath="Label"
DisplayMemberPath="Label" />
Here is my view Model:
public class ComboBoxViewModel : ViewModelBase
{
private string _selectedCode;
public ReadOnlyObservableCollection<ComboBoxItem> Choices { get; }
public ComboBoxItem Value
{
get { return this.Choices.FirstOrDefault(choice => choice.Code == _selectedCode); }
set
{
this.SetCode(value?.Code)
}
}
public string Text
{
get { return this.Value?.Label ?? _selectedCode; }
set
{
// Only set the code if no pre-defined code can be selected
if (this.Value == null)
{
this.SetCode(value)
}
}
}
public ComboBoxViewModel()
{
this.Choices = [..];
}
public bool SetCode(string code)
{
if (_selectedCode != code)
{
_selectedCode = code;
// Tried all the combination with/without/different order with no change
this.RaisePropertyChanged(nameof(this.Value));
this.RaisePropertyChanged(nameof(this.Text));
}
}
}
public class ComboBoxItem
{
public string Code { get; }
public string Label { get; }
public ComboBoxItem(string code, string label)
{
this.Code = code;
this.Label = label;
}
}
The Choices collection is initialized with some pair: Code,Label. I want to display the Label to the user and use the Code in my business layer. I also want my user to input its own code in the ComboBox (this is why the IsEditable dependency property is set to True and why I also bind Text on my ViewModel).
Everythings works fine when directly bind my ViewModel on the Control. The _selectedCode is updated prioritary with the selected Choices element or with the manual input if necessary.
My problem occurs when I pre-set the _selectedCode using the SetCode method. The Value property is no longer updated when I chose a new existing Choice in the ComboBox...
Is it possible to bind both SelectedItem and Text of a ComboBox? Do you have an idea why the bound properties are not updated after a programmatic initialization? It is like the event is not fired anymore...
I am really struggling with data binding and the MVVM Methodology, though I like the concept I am just struggling. I have created a WPF for that has multiple comboboxes and a button. The first combobox will list database instance names. the remaining comboboxes will be populated after the button is clicked. Since I am having issues with the first, database instances, combobox I will only show my code for that. When the application starts up the combobox is loaded and the first item is selected, as expected. The issue is when I select a new name my method that I expect to get called does not. Can someone help me to understand why my method public DBInstance SelectedDBInstance is not getting executed when I have this in my XAML, SelectedValue="{Binding SelectedDBInstance, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}?
Here is my XAML for the database instances combobox. One question I have here is the "value" fpr SelectedValuePath, if I change it to say "DBInstanceName" it does not work.
<ComboBox x:Name="cbxRLFDBInstances" ItemsSource="{Binding DBInstances}"
SelectedValue="{Binding SelectedDBInstance, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="value" DisplayMemberPath="DBInstanceName"/>
Here is my ViewModel Code:
namespace DatabaseTest.ViewModel
{
class RLFDatabaseTableViewModel : INotifyPropertyChanged
{
Utilities dbtUtilities = new Utilities();
public RelayCommand LoadDBInfoCommand
{
get;
set;
}
public RLFDatabaseTableViewModel()
{
LoadDBInstances();
LoadDBInfoCommand = new RelayCommand(LoadDBInfo);
}
public ObservableCollection<DBInstance> DBInstances
{
get;
set;
}
public void LoadDBInstances()
{
ObservableCollection<DBInstance> dbInstances = new ObservableCollection<DBInstance>();
DataTable dt = SmoApplication.EnumAvailableSqlServers(false);
dbInstances.Add(new DBInstance { DBInstanceName = "fal-conversion\\mun2012ci" });
dbInstances.Add(new DBInstance { DBInstanceName = "fal-conversion\\mun2014ci" });
if (dt.Rows.Count > 0)
{
foreach (DataRow dr in dt.Rows)
{
dbInstances.Add(new DBInstance { DBInstanceName = dr["Name"].ToString() });
}
}
DBInstances = dbInstances;
}
private DBInstance _selectedDBInstance;
public DBInstance SelectedDBInstance
{
get
{
return _selectedDBInstance;
}
set
{
_selectedDBInstance = value;
RaisePropertyChanged("SelectedDBInstance");
//ClearComboBoxes();
}
}
}
}
Here is my Model code. When I step through the code this method, public string DBInstanceName, gets executed multiple time. I do not know why and it is seems wasteful to me.
namespace DatabaseTest.Model
{
public class RLFDatabaseTableModel { }
public class DBInstance : INotifyPropertyChanged
{
private string strDBInstance;
public override string ToString()
{
return strDBInstance;
}
public string DBInstanceName
{
get
{
return strDBInstance;
}
set
{
if (strDBInstance != value)
{
strDBInstance = value;
RaisePropertyChanged("DBInstanceName");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
You should bind the SelectedItem property of the ComboBox to the SelectedDBInstance property and get rid of the SelectedValuePath:
<ComboBox x:Name="cbxRLFDBInstances" ItemsSource="{Binding DBInstances}"
SelectedItem="{Binding SelectedDBInstance, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="DBInstanceName"/>
The SelectedValuePath property is only used when you want to bind to a source property that is not of the same type as the item in the ItemsSource collection.
To select an item initially you should set the SelectedDBInstance property to an item that is present in the DBInstances collection:
public RLFDatabaseTableViewModel()
{
LoadDBInstances();
LoadDBInfoCommand = new RelayCommand(LoadDBInfo);
SelectedDBInstance = DBInstances[0]; //selected the first item
}
I am working with DataGrids but I am struggling to binding my data since the number of columns varies depending of the info that has to be showed.
So, what I have tried to do is to create and object which contains all the columns and rows that I need at some point and binding this object to the ItemsSource property. Since I have worked with DataGridViews in WindowsForms I have in mind something like this:
DataTable myTable = new DataTable();
DataColumn col01 = new DataColumn("col 01");
myTable.Columns.Add(col01);
DataColumn col02 = new DataColumn("col 02");
myTable.Columns.Add(col02);
DataRow row = myTable.NewRow();
row[0] = "data01";
row[1] = "data02";
myTable.Rows.Add(row);
row = myTable.NewRow();
row[0] = "data01";
row[1] = "data02";
myTable.Rows.Add(row);
But I haven't been able to find a way to do the same thing in WPF since I need some columns to be DataGridComboBoxColumns for example.
Actually I have read many post about it in this site, but none of them helped to me. I am really lost.
Could anyone help me? I just need to be able to create a table which may contain DataGridTextColumns or `DataGridComboBoxColumns, etc, In order to bind this final object to the DataGrid's ItemsSource property.
Hope someone can help me.
Okay, let me try to take an example which is similar to your needs
Let's assume we use this class:
public class MyObject
{
public int MyID;
public string MyString;
public ICommand MyCommand;
}
And we are willing to display a DataGrid listing the ID, and having as a second column a Button, with the property MyString as content, which, when clicked, launches the ICommand MyCommand which opens in a new window whatever you want.
Here is what you should have on the View side:
<DataGrid ItemsSource="{Binding MyList}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding MyID}" />
<DataGridTemplateColumn Header="Buttons">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding MyString}" Command="{Binding MyCommand}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
This will show a DataGrid taking all the content in an IEnumerable<MyObject> named 'MyList', and shows two columns as defined before.
Now if you need to define the command.
First, I recommend you read this introductory link to MVVM and take the RelayCommand class (that's what we're gonna use for your problem)
So, in your ViewModel, the one which defines the MyList, here is how you should define some of the useful objects:
public ObservableCollection<MyObject> MyList { get; set; }
// blah blah blah
public void InitializeMyList()
{
MyList = new ObservableCollection<MyObject>();
for (int i = 0; i < 5; i++)
{
MyList.Add(InitializeMyObject(i));
}
}
public MyObject InitializeMyObject(int i)
{
MyObject theObject = new MyObject();
theObject.MyID = i;
theObject.MyString = "The object " + i;
theObject.MyCommand = new RelayCommand(param =< this.ShowWindow(i));
return theObject
}
private void ShowWindow(int i)
{
// Just as an exammple, here I just show a MessageBox
MessageBox.Show("You clicked on object " + i + "!!!");
}
A simple example of binding to a ObservableCollection of a custom object. Add more properties to the custom object to match what you want your rows to look like.
using System.Collections.ObjectModel;
public MyClass
{
public ObservableCollection<MyObject> myList { get; set; }
public MyClass()
{
this.DataContext = this;
myList = new ObservableCollection<MyObject>();
myList.Add(new MyObject() { MyProperty = "foo", MyBool = false };
myList.Add(new MyObject() { MyProperty = "bar", MyBool = true };
}
}
public MyObject
{
public string MyProperty { get; set; }
// I believe will result in checkbox in the grid
public bool MyBool { get; set; }
//...as many properties as you want
}
with xaml
<DataGrid ItemsSource= "{Binding myList}" />
Might be some small syntax errors, I wrote that entirely within the SO window.
I am new to WPF and used Damascus's example to learn binding of a List to a datagrid. But when I used his answer I found that my datagrid would populate with the correct number of rows but not with any of the properties from the MyObject class. I did a bit more searching then stumbled across what I had to do by accident.
I had to encapsulate the MyObject class properties to have them show. It wasn't enough to have them be public.
Before:
public class MyObject
{
public int MyID;
public string MyString;
public ICommand MyCommand;
}
After:
public class MyObject
{
private int _myID;
public int MyID
{
get { return _myID; }
set { _myID = value; }
}
private string _myString;
public string MyString
{
get { return _myString; }
set { _myString = value; }
}
private ICommand _myCommand;
public ICommand MyCommand
{
get { return _myCommand; }
set { _myCommand = value; }
}
}
Thank you Damascus for a great example and thank you Dante for a great question. I don't know if this is due to a change in version since your post but hopefully this will help others new to WPF like me.
I'm not quite sure how to deal with this problem. I'm using a bunch of comboboxes with dropdown lists of values we allow the user to set a property too. (i.e. Currencies = "USD, CAD, EUR").
Every now and then, when we load data, we'll find the currency is something not in our list, like "AUD". In this case, we still want the combobox to display the loaded value, and the current selected Currency should remain "AUD" unless the user chooses to change it, in which case their only options will still be "USD, CAD, EUR".
My problem is that as soon as the control becomes visible, the ComboBox is calling the setter on my SelectedCurrency property and setting it to null, presumably because the current value "AUD" isn't in it's list. How can I disable this behaviour without making it possible for the user to type whatever they want into the Currency field?
Set IsEditable="True", IsReadOnly="True", and your SelectedItem equal to whatever object you want to hold the selected item
<ComboBox ItemsSource="{Binding SomeCollection}"
Text="{Binding CurrentValue}"
SelectedItem="{Binding SelectedItem}"
IsEditable="True"
IsReadOnly="True">
IsEditable allows the Text property to show a value not in the list
IsReadOnly makes it so the Text property is not editable
And SelectedItem stores the selected item. It will be null until the user selects an item in the list, so in your SaveCommand, if SelectedItem == null then use CurrentValue instead of SelectedItem when saving to the database
This seems to be a reasonably common problem. Imagine you have a lookup list in the database, maybe a list of employees. The employee table has a 'works here' flag. Another table references the employee lookup list. When a person leaves the company, you want your views to show the name of the old employee, but not allow the old employee to be assigned in future.
Here's my solution to the similar currency problem:
Xaml
<Page.DataContext>
<Samples:ComboBoxWithObsoleteItemsViewModel/>
</Page.DataContext>
<Grid>
<ComboBox Height="23" ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"/>
</Grid>
C#
// ViewModelBase and Set() are from MVVM Light Toolkit
public class ComboBoxWithObsoleteItemsViewModel : ViewModelBase
{
private readonly string _originalCurrency;
private ObservableCollection<string> _items;
private readonly bool _removeOriginalWhenNotSelected;
private string _selectedItem;
public ComboBoxWithObsoleteItemsViewModel()
{
// This value might be passed in to the VM as a parameter
// or obtained from a data service
_originalCurrency = "AUD";
// This list is hard-coded or obtained from your data service
var collection = new ObservableCollection<string> {"USD", "CAD", "EUR"};
// If the value to display isn't in the list, then add it
if (!collection.Contains(_originalCurrency))
{
// Record the fact that we may need to remove this
// value from the list later.
_removeOriginalWhenNotSelected = true;
collection.Add(_originalCurrency);
}
Items = collection;
SelectedItem = _originalCurrency;
}
public string SelectedItem
{
get { return _selectedItem; }
set
{
// Remove the original value from the list if necessary
if(_removeOriginalWhenNotSelected && value != _originalCurrency)
{
Items.Remove(_originalCurrency);
}
Set(()=>SelectedItem, ref _selectedItem, value);
}
}
public ObservableCollection<string> Items
{
get { return _items; }
private set { Set(()=>Items, ref _items, value); }
}
}
You should set IsEditable of the ComboBox to true and bind the Text property instead of the SelectedValue property.
If IsEditable = false then ComboBox does not support a value not in the list.
If you want user action to add a value but not edit that value or any existing values then one approach might be to put the new value in TextBlock (not editable) and a Button to let them add that value. If they select any value from the combobox then hide the TextBlock and Button.
Another approach would be to add the value to the list with more complex logic behind that if any another value is selected then that tentative value is removed. And the tentative value does not get persisted until it is selected.
He doesn't want to be users to be able to type, so IsEditable seems to be off the table.
What I would do is just add the new value AUD to the Item list as
ComboBoxItem Content="AUD" Visibility="Collapsed"
Then Text="AUD" will work in code but not from the drop down.
To be fancy, one could make a converter for the ItemsSource that binds to the TEXT box and adds it collapsed automatically
Here was my solution to this problem:
XAML looks like this:
<DataTemplate>
<local:CCYDictionary Key="{TemplateBinding Content}">
<local:CCYDictionary.ContentTemplate>
<DataTemplate>
<ComboBox Style="{StaticResource ComboBoxCellStyle}"
SelectedValuePath="CCYName"
DisplayMemberPath="CCYName"
TextSearch.TextPath="CCYName"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:CCYDictionary}}, Path=ListItems}"
SelectedValue="{Binding}" />
</DataTemplate>
</local:CCYDictionary.ContentTemplate>
</local:CCYDictionary>
</DataTemplate>
<!-- For Completion's sake, here's the style and the datacolumn using it -->
<Style x:Key="ComboBoxCellStyle" TargetType="ComboBox">
<Setter Property="IsEditable" Value="False"/>
<Setter Property="IsTextSearchEnabled" Value="True"/>
<!-- ...other unrelated stuff (this combobox was was a cell template for a datagrid) -->
</Style>
<Column FieldName="CCYcode" Title="Currency" DataTemplate="{StaticResource CCYEditor}" />
There's probably a nicer way for the dictionary to expose the ItemsSource so that the Bindings aren't so ugly, but once I got it to work I was too tired of the problem to refine it further.
Individual dictionaries declared like so:
public class CCYDictionary : DataTableDictionary<CCYDictionary>
{
protected override DataTable table { get { return ((App)App.Current).ApplicationData.CCY; } }
protected override string indexKeyField { get { return "CCY"; } }
public CCYDictionary() { }
}
public class BCPerilDictionary : DataTableDictionary<BCPerilDictionary>
{
protected override DataTable table { get { return ((App)App.Current).ApplicationData.PerilCrossReference; } }
protected override string indexKeyField { get { return "BCEventGroupID"; } }
public BCPerilDictionary() { }
}
//etc...
Base class looks like:
public abstract class DataTableDictionary<T> : ContentPresenter where T : DataTableDictionary<T>
{
#region Dependency Properties
public static readonly DependencyProperty KeyProperty = DependencyProperty.Register("Key", typeof(object), typeof(DataTableDictionary<T>), new PropertyMetadata(null, new PropertyChangedCallback(OnKeyChanged)));
public static readonly DependencyProperty RowProperty = DependencyProperty.Register("Row", typeof(DataRowView), typeof(DataTableDictionary<T>), new PropertyMetadata(null, new PropertyChangedCallback(OnRowChanged)));
public static readonly DependencyProperty ListItemsProperty = DependencyProperty.Register("ListItems", typeof(DataView), typeof(DataTableDictionary<T>), new PropertyMetadata(null));
public static readonly DependencyProperty IndexedViewProperty = DependencyProperty.Register("IndexedView", typeof(DataView), typeof(DataTableDictionary<T>), new PropertyMetadata(null));
#endregion Dependency Properties
#region Private Members
private static DataTable _SourceList = null;
private static DataView _ListItems = null;
private static DataView _IndexedView = null;
private static readonly Binding BindingToRow;
private static bool cachedViews = false;
private bool m_isBeingChanged;
#endregion Private Members
#region Virtual Properties
protected abstract DataTable table { get; }
protected abstract string indexKeyField { get; }
#endregion Virtual Properties
#region Public Properties
public DataView ListItems
{
get { return this.GetValue(ListItemsProperty) as DataView; }
set { this.SetValue(ListItemsProperty, value); }
}
public DataView IndexedView
{
get { return this.GetValue(IndexedViewProperty) as DataView; }
set { this.SetValue(IndexedViewProperty, value); }
}
public DataRowView Row
{
get { return this.GetValue(RowProperty) as DataRowView; }
set { this.SetValue(RowProperty, value); }
}
public object Key
{
get { return this.GetValue(KeyProperty); }
set { this.SetValue(KeyProperty, value); }
}
#endregion Public Properties
#region Constructors
static DataTableDictionary()
{
DataTableDictionary<T>.BindingToRow = new Binding();
DataTableDictionary<T>.BindingToRow.Mode = BindingMode.OneWay;
DataTableDictionary<T>.BindingToRow.Path = new PropertyPath(DataTableDictionary<T>.RowProperty);
DataTableDictionary<T>.BindingToRow.RelativeSource = new RelativeSource(RelativeSourceMode.Self);
}
public DataTableDictionary()
{
ConstructDictionary();
this.SetBinding(DataTableDictionary<T>.ContentProperty, DataTableDictionary<T>.BindingToRow);
}
#endregion Constructors
#region Private Methods
private bool ConstructDictionary()
{
if( cachedViews == false )
{
_SourceList = table;
if( _SourceList == null )
{ //The application isn't loaded yet, we'll have to defer constructing this dictionary until it's used.
return false;
}
_SourceList = _SourceList.Copy(); //Copy the table so if the base table is modified externally we aren't affected.
_ListItems = _SourceList.DefaultView;
_IndexedView = CreateIndexedView(_SourceList, indexKeyField);
cachedViews = true;
}
ListItems = _ListItems;
IndexedView = _IndexedView;
return true;
}
private DataView CreateIndexedView(DataTable table, string indexKey)
{
// Create a data view sorted by ID ( keyField ) to quickly find a row.
DataView dataView = new DataView(table);
dataView.Sort = indexKey;
dataView.ApplyDefaultSort = true;
return dataView;
}
#endregion Private Methods
#region Static Event Handlers
private static void OnKeyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
// When the Key changes, try to find the data row that has the new key.
// If it is not found, return null.
DataTableDictionary<T> dataTableDictionary = sender as DataTableDictionary<T>;
if( dataTableDictionary.m_isBeingChanged ) return; //Avoid Reentry
dataTableDictionary.m_isBeingChanged = true;
try
{
if( dataTableDictionary.IndexedView == null ) //We had to defer loading.
if( !dataTableDictionary.ConstructDictionary() )
return; //throw new Exception("Dataview is null. Check to make sure that all Reference tables are loaded.");
DataRowView[] result = _IndexedView.FindRows(dataTableDictionary.Key);
DataRowView dataRow = result.Length > 0 ? result[0] : null;
//Sometimes a null key is valid - but sometimes it's just xceed being dumb - so we only skip the following step if it wasn't xceed.
if( dataRow == null && dataTableDictionary.Key != null )
{
//The entry was not in the DataView, so we will add it to the underlying table so that it is not nullified. Treaty validation will take care of notifying the user.
DataRow newRow = _SourceList.NewRow();
//DataRowView newRow = _IndexedView.AddNew();
int keyIndex = _SourceList.Columns.IndexOf(dataTableDictionary.indexKeyField);
for( int i = 0; i < _SourceList.Columns.Count; i++ )
{
if( i == keyIndex )
{
newRow[i] = dataTableDictionary.Key;
}
else if( _SourceList.Columns[i].DataType == typeof(String) )
{
newRow[i] = "(Unrecognized Code: '" + (dataTableDictionary.Key == null ? "NULL" : dataTableDictionary.Key) + "')";
}
}
newRow.EndEdit();
_SourceList.Rows.InsertAt(newRow, 0);
dataRow = _IndexedView.FindRows(dataTableDictionary.Key)[0];
}
dataTableDictionary.Row = dataRow;
}
catch (Exception ex)
{
throw new Exception("Unknow error in DataTableDictionary.OnKeyChanged.", ex);
}
finally
{
dataTableDictionary.m_isBeingChanged = false;
}
}
private static void OnRowChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
// When the Key changes, try to find the data row that has the new key.
// If it is not found, return null.
DataTableDictionary<T> dataTableDictionary = sender as DataTableDictionary<T>;
if( dataTableDictionary.m_isBeingChanged ) return; //Avoid Reentry
dataTableDictionary.m_isBeingChanged = true;
try
{
if( dataTableDictionary.Row == null )
{
dataTableDictionary.Key = null;
}
else
{
dataTableDictionary.Key = dataTableDictionary.Row[dataTableDictionary.indexKeyField];
}
}
catch (Exception ex)
{
throw new Exception("Unknow error in DataTableDictionary.OnRowChanged.", ex);
}
finally
{
dataTableDictionary.m_isBeingChanged = false;
}
}
#endregion Static Event Handlers
}