Prevent ComboBox from setting selectedValue to null when not in constraint list
I'm not really sure how to handle this problem. I use a bunch of comboboxes with dropdowns that we allow the user to set a property too. (ie Currencies = "USD, CAD, EUR").
From time to time, when we load data, we find that the currency is something not on our list, for example "AUD". In this case, we still want the combobox to display the loaded value and the currently selected currency should remain "AUD" unless the user wants to change it, in which case their only parameters would be "USD, CAD, EUR" anyway.
My problem is that as soon as the control becomes visible the ComboBox calls the setter on the SelectedCurrency property and sets it to null
, presumably because the current "AUD" value is not listed. How can I turn off this behavior without allowing the user to enter whatever they want in the Currency field?
source to share
This seems to be a fairly common problem. Imagine you have a database search list, perhaps a list of employees. The employee table has a "works here" flag. Another table refers to the employee search list. When a person leaves a company, you want your views to reflect the old employee's name, but not allow the old employee to be appointed in the future.
Here's my solution to a similar currency problem:
Xaml
<Page.DataContext>
<Samples:ComboBoxWithObsoleteItemsViewModel/>
</Page.DataContext>
<Grid>
<ComboBox Height="23" ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"/>
</Grid>
FROM#
// 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); }
}
}
source to share
Set IsEditable="True"
, IsReadOnly="True"
and yours SelectedItem
is equal to whatever object you want to keep the selected item
<ComboBox ItemsSource="{Binding SomeCollection}"
Text="{Binding CurrentValue}"
SelectedItem="{Binding SelectedItem}"
IsEditable="True"
IsReadOnly="True">
IsEditable
allows the property to Text
show a value not in the list
IsReadOnly
makes the property Text
not editable
And it SelectedItem
saves the selected item. This will be null
until the user selects an item in the list, so in SaveCommand
if SelectedItem == null
use CurrentValue
instead SelectedItem
when saving to the database
source to share
If IsEditable = false, the ComboBox does not support a value not listed.
If you want the user action to add a value, but not edit that value or any existing values, one approach would be to add a new value to the TextBlock (not editable) and Button and have them add that value. If they choose any value from the dropdown list, then hide the TextBlock and Button.
Another approach would be to add the value to the list with more complex logic, if any other value was selected then that pre-value is removed. And the preliminary value is not saved until it is selected.
source to share
It doesn't want users to be able to print, so IsEditable doesn't seem to work.
What I would do is just add the new AUD value to the Item list as
ComboBoxItem Content="AUD" Visibility="Collapsed"
Then Text = "AUD" will work in the code, but not from the dropdown list. To be a fantasy, one could make a converter for ItemsSource that binds to the TEXT window and adds that it crashed automatically
source to share
Here is my solution to this problem:
The 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 sake, here 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}" />
Probably the best way for the dictionary to open the ItemsSource object so that the Bindings aren't so ugly, but once I got it to work I was too tired of the problem to modify it.
Individual dictionaries are named as follows:
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...
The base class looks like this:
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 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 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
}
source to share