I am trying to create a Datagrid matrix that adds columns with Bindings during runtime.
I'm working in Visual Studio 2019, with C#, WPF and MVVM.
The matrix looks like this:
The fist three rows are created inside the .xaml of the View, while the last two columns are created dynamically. In another View there's a similar datagrid and each row added there results in a column in this matrix.
The usual columns are created like this:
<DataGridTextColumn Header="Name" Binding="{Binding EntryName, UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
The dynamically added columns are added like this inside of the code behind file of the View:
foreach(IControlRoleViewModel item in vm.ControlRoleTable)
{
if (item.EntryName != null)
{
MyGrid.Columns.Add(new DataGridCheckBoxColumn { Header = item.EntryName });
}
}
Since the new columns are getting dynamically added I'm not sure how to create dynamic bindings. When I'm using an array I have to define how long it is, and when I tried using a list or an ObservableCollection I failed because of other circumstances.
I need the information of the checkbox in the corresponding row. I already get the information which row is clicked, through the ObservableCollection representing the whole DataGrid.
What I tried was this:
foreach(IControlRoleViewModel item in vm.ControlRoleTable)
{
if (item.EntryName != null)
{
_newEntry = item.EntryName + "_IsTrue";
//send Binding Definition to ViewModel
vm.newEntry.Add(_newEntry); //vm.newEntry is a List<string> in the ViewModel
MyGrid.Columns.Add(new DataGridCheckBoxColumn { Header = item.EntryName, Binding = new Binding(_newEntry) });
}
}
That way I got the information of the new Binding into the ViewModel. But since the DataGrid representing the whole Matrix is defined like this:
<DataGrid Name="MyGrid" ItemsSource="{Binding ProceduresTable }" SelectionMode="Single" SelectedItem="{Binding SelectedEntryProcedures}" AutoGenerateColumns="false" CanUserAddRows="True" IsReadOnly="False" >
The Binding could not be found inside of the ObservableCollection<IProceduresTable> ProceduresTable.
After that I tried creating a new ObservableCollection inside of the IProceduresTable Interface. But that failed since I couldn't find out how to get a binding into an ObservableCollection that itself is inside an ObservableCollection.
Is there an easier way to do this?
For such cases I use my GenericRow and GenericTable classes:
You can use these two classes to create dynamic rows and columns. Using GenericRow class, you can generate row with desired property name and you can use the same property name for the columns for suitable binding.
public class GenericRow : CustomTypeDescriptor, INotifyPropertyChanged
{
#region Private Fields
List<PropertyDescriptor> _property_list = new List<PropertyDescriptor>();
#endregion
#region INotifyPropertyChange Implementation
public event PropertyChangedEventHandler PropertyChanged = delegate { };
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion INotifyPropertyChange Implementation
#region Public Methods
public void SetPropertyValue<T>(string propertyName, T propertyValue)
{
var properties = this.GetProperties()
.Cast<PropertyDescriptor>()
.Where(prop => prop.Name.Equals(propertyName));
if (properties == null || properties.Count() != 1)
{
throw new Exception("The property doesn't exist.");
}
var property = properties.First();
property.SetValue(this, propertyValue);
OnPropertyChanged(propertyName);
}
public T GetPropertyValue<T>(string propertyName)
{
var properties = this.GetProperties()
.Cast<PropertyDescriptor>()
.Where(prop => prop.Name.Equals(propertyName));
if (properties == null || properties.Count() != 1)
{
throw new Exception("The property doesn't exist.");
}
var property = properties.First();
return (T)property.GetValue(this);
}
public void AddProperty<T, U>(string propertyName) where U : GenericRow
{
var customProperty =
new CustomPropertyDescriptor<T>(
propertyName,
typeof(U));
_property_list.Add(customProperty);
}
#endregion
#region Overriden Methods
public override PropertyDescriptorCollection GetProperties()
{
var properties = base.GetProperties();
return new PropertyDescriptorCollection(
properties.Cast<PropertyDescriptor>()
.Concat(_property_list).ToArray());
}
#endregion
}
and Generic Table:
public class GenericTable
{
private string tableName = "";
public string TableName
{
get { return tableName; }
set { tableName = value; }
}
private ObservableCollection<DataGridColumn> columnCollection;
public ObservableCollection<DataGridColumn> ColumnCollection
{
get { return columnCollection; }
private set { columnCollection = value; }
}
private ObservableCollection<GenericRow> genericRowCollection;
public ObservableCollection<GenericRow> GenericRowCollection
{
get { return genericRowCollection; }
set { genericRowCollection = value; }
}
public GenericTable(string tableName)
{
this.TableName = tableName;
ColumnCollection = new ObservableCollection<DataGridColumn>();
GenericRowCollection = new ObservableCollection<GenericRow>();
}
/// <summary>
/// ColumnName is also binding property name
/// </summary>
/// <param name="columnName"></param>
public void AddColumn(string columnName)
{
DataGridTextColumn column = new DataGridTextColumn();
column.Header = columnName;
column.Binding = new Binding(columnName);
ColumnCollection.Add(column);
}
public override string ToString()
{
return TableName;
}
}
And for the XAML side:
<DataGrid Name="dataGrid"
local:DataGridColumnsBehavior.BindableColumns="{Binding ColumnCollection}"
AutoGenerateColumns="False"
...>
and finally DataGridColumsBehaior :
public class DataGridColumnsBehavior
{
public static readonly DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached("BindableColumns",
typeof(ObservableCollection<DataGridColumn>),
typeof(DataGridColumnsBehavior),
new UIPropertyMetadata(null, BindableColumnsPropertyChanged));
private static void BindableColumnsPropertyChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = source as DataGrid;
ObservableCollection<DataGridColumn> columns = e.NewValue as ObservableCollection<DataGridColumn>;
dataGrid.Columns.Clear();
if (columns == null)
{
return;
}
foreach (DataGridColumn column in columns)
{
dataGrid.Columns.Add(column);
}
columns.CollectionChanged += (sender, e2) =>
{
NotifyCollectionChangedEventArgs ne = e2 as NotifyCollectionChangedEventArgs;
if (ne.Action == NotifyCollectionChangedAction.Reset)
{
dataGrid.Columns.Clear();
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Add)
{
foreach (DataGridColumn column in ne.NewItems)
{
dataGrid.Columns.Add(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Move)
{
dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
foreach (DataGridColumn column in ne.OldItems)
{
dataGrid.Columns.Remove(column);
}
}
else if (ne.Action == NotifyCollectionChangedAction.Replace)
{
dataGrid.Columns[ne.NewStartingIndex] = ne.NewItems[0] as DataGridColumn;
}
};
}
public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumn> value)
{
element.SetValue(BindableColumnsProperty, value);
}
public static ObservableCollection<DataGridColumn> GetBindableColumns(DependencyObject element)
{
return (ObservableCollection<DataGridColumn>)element.GetValue(BindableColumnsProperty);
}
}
In the end, you can create GenericRows and add them into GenericTable. It is like a small viewmodel. In XAML side, dont forget to bind rowscollection as itemsource of the DataGrid.
Related
In a WPF application, I have a view with an editable datagrid and a viewmodel. This is an existing codebase, and a problem arises: there are fields in the viewmodel that raises exceptions but those fields are not in the view. A refactor is necessary, but for now we need to implement a visual clue (red border around the row) for the user as a quick fix.
From the viewmodel, I raise an event that a validation took place, and in the code-behind, I want to check in the datagridrow if the ValidationErrorTemplate is enabled.
As the elements added by the ValidationErrorTemplate are added as AdornerLayer outside of the datagridrows, it seems that I have no clue to which datagridrow this is coupled?
I have not much code to show, just that I get to the correct datagridrow for which viewmodel a validation took place:
private void OnValidationEvent(ValidationEventArgs e)
{
var rows = BoekingDatagrid.GetDataGridRow(e.ID);
if (e.HasErrors)
{
if (errorBorder == null)
{
row.BorderBrush = new SolidColorBrush(Colors.Red);
row.BorderThickness = new Thickness(1);
var vm = row.DataContext as ItemBaseViewModel;
LogValidationErrors(vm, UserContext);
}
}
else
{
row.BorderThickness = new Thickness(0);
}
}
Every column has the following xaml, with a Validation.ErrorTemplate:
<DataGridTemplateColumn Header="Name"
CanUserResize="False"
SortMemberPath ="Name"
Width="130"
MinWidth="130">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
IsEnabled="{Binding ViewMode, Converter={StaticResource ViewModeToBoolean}, ConverterParameter=name}"
Validation.ErrorTemplate="{StaticResource ResourceKey=ErrorTemplate2_Grid}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
ErrorTemplate2_Grid adds a red border and tooltip to the cell.
In Visual Live Tree, you can see that the rows and the error visuals, but they are not nested:
The question is: how can I find out if there are visual error elements added to the datagridrow, when the viewmodel is invalid?
Not sure what BindingGroup exactly does, but the databound rows does have there own BindingGroups with only the databindings of the controls in the row. And a very convenient property: HasValidationError
private void OnValidationEvent(ValidationEventArgs e)
{
var row = BoekingDatagrid.GetDataGridRow();
if (row != null)
{
if (e.HasErrors)
{
if (!row.BindingGroup.HasValidationError)
{
row.BorderBrush = new SolidColorBrush(Colors.Red);
row.BorderThickness = new Thickness(1);
}
else
{
row.BorderThickness = new Thickness(0);
}
}
else
{
row.BorderThickness = new Thickness(0);
}
}
}
Answered for mine and others future reference.
Validation in WPF is typically done using the interface INotifyDataErrorInfo. There are other ways too, but the ViewModel raising an event that the View handles isn't typically one of them. A very simplified model class could look like this:
public class Model : INotifyDataErrorInfo
{
public Model(int myInt, string myString)
{
MyInt = myInt;
MyString = myString;
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors
{
get
{
try
{
return MyInt == 88 || MyString == "foo";
}
catch (Exception)
{
return true;
}
}
}
public int MyInt
{
get { throw new NotImplementedException(); }
set { }
}
public string MyString { get; set; }
public IEnumerable GetErrors(string propertyName)
{
try
{
if (propertyName == nameof(MyInt) && MyInt == 88)
return new string[] { "MyInt must not be 88" };
if (propertyName == nameof(MyString) && MyString == "foo")
return new string[] { "MyString must not be 'foo'" };
return new string[0];
}
catch (Exception)
{
return new string[] { "Exception" };
}
}
}
A quick and diry window could look like this:
public partial class MainWindow : Window
{
public MainWindow()
{
Models = new List<Model>
{
new Model(1,"hello"),
new Model(1,"foo"),
new Model(88,"hello"),
new Model(2,"world"),
};
DataContext = this;
InitializeComponent();
}
public List<Model> Models { get; set; }
}
whereas the XAML just contains <DataGrid ItemsSource="{Binding Models}" />
A red rectangle in case of an error is default. Just apply your custom template and do not set any ValidationErrorTemplate.
I have a ListView in XAML that is bound to an ObservableCollection in the ViewModel. Upon initialization or OnAppearing() the ListView items are displayed perfectly.
However, when I try to update the ListView items from within the page (through ViewModel) the items are updated but the old items are still there.
Basically, the new items are added to the ListView but below the items that were in the ObservableCollection before. I have implemented INotifyPropertyChanged and I think I have done everything correct (although clearly not).
Please tell me what I'm doing wrong. I've tried Clear() on the Collection but to no avail (same outcome).
BaseViewModel:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void SetValue<T>(ref T backingField, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(backingField, value))
return;
backingField = value;
OnPropertyChanged(propertyName);
}
}
XAML:
<ListView IsEnabled="{Binding IsLoadingTable, Converter={Helpers:InverseBoolConverter}}"
IsVisible="{Binding IsLoadingTable, Converter={Helpers:InverseBoolConverter}}"
ItemsSource="{Binding LeagueStandings}"
SelectedItem="{Binding SelectedTeam, Mode=TwoWay}"
ItemSelected="ListView_ItemSelected"
RowHeight="60"
SeparatorVisibility="Default"
SeparatorColor="{DynamicResource accentColor}">
Page.cs:
protected override void OnAppearing()
{
base.OnAppearing();
ViewModel.LoadLeagueStandingsCommand.Execute(_divisionId);
ViewModel.LoadPickerItemsCommand.Execute(null);
}
ViewModel Initialization:
private ObservableCollection<Standing> _leagueStandings;
public ObservableCollection<Standing> LeagueStandings
{
get { return _leagueStandings ?? (_leagueStandings = new ObservableCollection<Standing>()); }
set { SetValue(ref _leagueStandings, value); }
}
ViewModel Methods:
private async Task LoadLeagueStandings(string divId)
{
if (_hasLoadedStandings)
return;
if (IsLoadingTable)
return;
_hasLoadedStandings = true;
_divisionId = divId;
try
{
IsLoadingTable = true;
await _pageService.DisplayAlert("loading Selected", _divisionId, "ok");
var v = await GetLeagueTableAsync(_htmlParser, _divisionId);
LeagueStandings = new ObservableCollection<Standing>(v);
}
catch (Exception)
{
System.Diagnostics.Debug.WriteLine("Exception caught in DivisionsViewModel.cs.(LoadLeagueStandings).");
}
finally
{
IsLoadingTable = false;
}
}
ViewModel method called when Picker item changes:
private async Task SelectItem(string item)
{
if (item == null)
return;
SelectedItem = null;
var id = await _divisionFinder.GetDivisionIdAsync(item);
var v = await GetLeagueTableAsync(_htmlParser, id);
LeagueStandings = new ObservableCollection<Standing>(v);
}
Edit* - Picture of outcome. First collection ends at number 5 and new collections appends to end of listview starting at 1 again.
ListView Image
Edit 2:
public async Task<IEnumerable<Standing>> GetLeagueTableAsync(string divisionId)
{
// todo: get division Id from picker
string address = "";
if (IsOnline)
{
if (divisionId != "")
address = $"{BaseUrls.LeagueStandings}{divisionId}";
try
{
var config = Configuration.Default.WithDefaultLoader();
var document = await BrowsingContext.New(config).OpenAsync(address);
var cSelector = "table[class='table table-striped table-hover table-bordered'] tr";
var table = document.QuerySelectorAll(cSelector).Skip(1);
int count = 0;
foreach (var c in table)
{
var cells = c.QuerySelectorAll("td").ToArray();
_leagueStandings.Add(new Standing(++count, cells[0].TextContent.Trim(), cells[1].TextContent.Trim(),
cells[2].TextContent.Trim(), cells[3].TextContent.Trim(),
cells[4].TextContent.Trim(), cells[5].TextContent.Trim(),
cells[6].TextContent.Trim(), cells[7].TextContent.Trim()));
}
}
catch(Exception e)
{
System.Diagnostics.Debug.WriteLine($"\n\n Exception caught LoadingLeagueTable - {e} \n\n");
}
}
return _leagueStandings;
Since you're not adding neither removing items, and you're replacing the reference you need to raise the event telling that the view has changed. Instead of your code, replace it by this
private ObservableCollection<Standing> _leagueStandings;
public ObservableCollection<Standing> LeagueStandings
{
get { return _leagueStandings; }
set {
_leagueStandings = value;
RaisePropertyChanged("LeagueStandings");
}
}
Also for future references, ObservableCollection already implements INotifyPropertyChanged so you don't need to SetValue(x)..
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
}
Say I have a BindingList<Person> where Person has a public string property called Name. Is there a way to effectively (if not directly) bind to the Current property (which is a Person object) and then index into the Name property?
I'm imagining a binding set up like
nameLabel.DataBindings.Add(
new Binding("Text", this.myBindingListSource, "Current.Name", true));
or
nameLabel.DataBindings.Add(
new Binding("Text", this.myBindingListSource.Current, "Name", true));
Both of these approaches produce runtime errors.
Currently, I am simply subscribing to the BindingList's CurrentChanged event and handling updates in there. This works but I would prefer the DataBinding approach if possible.
Using the NestedBindingProxy class provided below, you can do
nameLabel.DataBindings.Add(
new Binding("Text", new NestedBindingProxy(this.myBindingListSource, "Current.Name"), true));
Below is the c# code for NestedBindingProxy. The problem with WinForms data binding is it doesn't detect value changes when you use a navigation path that contains multiple properties. WPF does this though. So I created the class NestedBindingProxy that does the change detection and it exposes a property called "Value" that the windows binding can bind too. Whenever any property changes in the navigation path, the notify property changed event will fire for the "Value" property.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading;
namespace WindowsFormsApplication4
{
public sealed class NestedBindingProxy : INotifyPropertyChanged
{
class PropertyChangeListener
{
private readonly PropertyDescriptor _prop;
private readonly WeakReference _prevOb = new WeakReference(null);
public event EventHandler ValueChanged;
public PropertyChangeListener(PropertyDescriptor property)
{
_prop = property;
}
public object GetValue(object obj)
{
return _prop.GetValue(obj);
}
public void SubscribeToValueChange(object obj)
{
if (_prop.SupportsChangeEvents)
{
_prop.AddValueChanged(obj, ValueChanged);
_prevOb.Target = obj;
}
}
public void UnsubsctribeToValueChange()
{
var prevObj = _prevOb.Target;
if (prevObj != null)
{
_prop.RemoveValueChanged(prevObj, ValueChanged);
_prevOb.Target = null;
}
}
}
private readonly object _source;
private PropertyChangedEventHandler _subscribers;
private readonly List<PropertyChangeListener> _properties = new List<PropertyChangeListener>();
private readonly SynchronizationContext _synchContext;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
bool hadSubscribers = _subscribers != null;
_subscribers += value;
bool hasSubscribers = _subscribers != null;
if (!hadSubscribers && hasSubscribers)
{
ListenToPropertyChanges(true);
}
}
remove
{
bool hadSubscribers = _subscribers != null;
_subscribers -= value;
bool hasSubscribers = _subscribers != null;
if (hadSubscribers && !hasSubscribers)
{
ListenToPropertyChanges(false);
}
}
}
public NestedBindingProxy(object source, string nestedPropertyPath)
{
_synchContext = SynchronizationContext.Current;
_source = source;
var propNames = nestedPropertyPath.Split('.');
Type type = source.GetType();
foreach (var propName in propNames)
{
var prop = TypeDescriptor.GetProperties(type)[propName];
var propChangeListener = new PropertyChangeListener(prop);
_properties.Add(propChangeListener);
propChangeListener.ValueChanged += (sender, e) => OnNestedPropertyChanged(propChangeListener);
type = prop.PropertyType;
}
}
public object Value
{
get
{
object value = _source;
foreach (var prop in _properties)
{
value = prop.GetValue(value);
if (value == null)
{
return null;
}
}
return value;
}
}
private void ListenToPropertyChanges(bool subscribe)
{
if (subscribe)
{
object value = _source;
foreach (var prop in _properties)
{
prop.SubscribeToValueChange(value);
value = prop.GetValue(value);
if (value == null)
{
return;
}
}
}
else
{
foreach (var prop in _properties)
{
prop.UnsubsctribeToValueChange();
}
}
}
private void OnNestedPropertyChanged(PropertyChangeListener changedProperty)
{
ListenToPropertyChanges(false);
ListenToPropertyChanges(true);
var subscribers = _subscribers;
if (subscribers != null)
{
if (_synchContext != SynchronizationContext.Current)
{
_synchContext.Post(delegate { subscribers(this, new PropertyChangedEventArgs("Value")); }, null);
}
else
{
subscribers(this, new PropertyChangedEventArgs("Value"));
}
}
}
}
}
Try this:
Binding bind = new Binding("Text", myBindingListSource, "Current");
bind.Format += (s,e) => {
e.Value = e.Value == null ? "" : ((Person)e.Value).Name;
};
nameLabel.DataBindings.Add(bind);
I haven't tested it but it should work and I've been waiting for the feedback from you.
I stumbled across this by accident (four years after the original post), and after a quick read, I find that the accepted answer appears to be really over-engineered in respect to the OP's question.
I use this approach, and have posted an answer here to (hopefully) help anyone else who has the same problem.
The following should work (if infact this.myBindingListSource implements IBindingList)
I would just create a binding source to wrap my binding list (in my Form/View)
BindingSource bindingSource = new BindingSource(this.myBindingListSource, string.Empty);
And then just bind to the binding source like this:
nameLabel.DataBindings.Add(new Binding("Text", bindingSource, "Name"));
I think the OP's original code didn't work because there is no member called 'Current' on a BindingList (unless the OP has some sort of specialised type not mentioned).
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
}