WPF Bound Editable Combobox not changing ID on Text Change - c#

I'm no sure what the right way to bind the combobox control is to my view model.
I'm using a MVVM approach so in my viewmodel i'm loading all the CDType data and am binding their source in combination with the actual record properties model.CDType.id and model.CDType.name.
What happens is that when i change the text? - i keep my old id (from the load routine) and get a new text value from the combobox binding therefor always writing over the existing record instead of creating a new one.
How can i make my combobox set the id to 0 / undefined if the text isn't in the list? (manually? - kind of lame)
Anything will help - thanks!
TL;DR: Editable Combo-box not updating ID on text change.
Sample Xaml binding:
<ComboBox x:Name="ddlCDType"
IsTextSearchEnabled="True"
IsTextSearchCaseSensitive="False"
StaysOpenOnEdit="True"
TextSearch.TextPath="Name"
ItemsSource="{Binding CDTypes}"
SelectedValue="{Binding Assignment.CDType.ID}"
Text="{Binding Assignment.CDType.Name,
UpdateSourceTrigger=PropertyChanged,
NotifyOnValidationError=True,
ValidatesOnDataErrors=True}"
DisplayMemberPath="Name"
SelectedValuePath="ID"
IsEditable="True"
HorizontalAlignment="Left"
Margin="98,10,0,0"
VerticalAlignment="Top"
Width="136" />

as I can understand you need update the previous last selected value when the name of selected ComboBox item is changed manually and this name is not presented in the ComboBox ItemSource. Here is my suggestion, it is combination of your ComboBox control data binding and logic defined in the ViewModel.
Explanation of binding
Since the ComboBox ItemsSource is the collection of Cd (the collection of ComboModels), thus the TextSearch.TextPath binding should be defined as CDType.Name where the CdType is property defined in the ComboModel that describes the sub-model, and the Name is an actual search path which is describing the sub-model.
The ComboBox Text property is binded to AssignmentText property to trigger the updating logic when the combo is lost the focus (as defined in binding).
The ItemsSource is trivial as defined in your sample code.
Selected value will bring a whole model to update (in case we change the selected value name).
Since the ComboBox ItemsSource is the collection of Cd (let's call this ComboModel) the DisplayMemberPath binding should be defined as CDType.Name where the CdType is property defined in the ComboModel that describes the sub-model, and the Name is an actual search path which is describing the sub-model.
Xaml Code:
<Grid VerticalAlignment="Bottom">
<Grid.DataContext>
<comboBoxWhenNoAnySelectedHelpAttempt:ComboboxDataContext/>
</Grid.DataContext>
<StackPanel Orientation="Vertical">
<ComboBox x:Name="ddlCDType"
IsTextSearchEnabled="True"
IsTextSearchCaseSensitive="False"
StaysOpenOnEdit="True"
TextSearch.TextPath="CDType.Name"
Text="{Binding AssignmentText,
UpdateSourceTrigger=LostFocus, Mode=TwoWay,
ValidatesOnDataErrors=True}"
ItemsSource="{Binding CDTypes}"
SelectedValue="{Binding Assignment}"
DisplayMemberPath="CDType.Name"
IsEditable="True"
HorizontalAlignment="Left"
Margin="98,10,0,0"
VerticalAlignment="Top"
Width="136" />
<!--added to see changes in updated combo box item-->
<TextBlock >
<Run Text="Name:"/>
<Run Text="{Binding Assignment.CDType.Name, UpdateSourceTrigger=PropertyChanged}"></Run>
<Run Text="Id:"/>
<Run Text="{Binding Assignment.CDType.ID, UpdateSourceTrigger=PropertyChanged}"></Run>
</TextBlock>
</StackPanel>
</Grid>
VM code
public class ComboboxDataContext:BaseObservableObject
{
private ObservableCollection<ComboModel> _cdTypes;
private ComboModel _assignment;
private string _assignmentText;
private string _lastAcceptedName;
public ComboboxDataContext()
{
CDTypes = new ObservableCollection<ComboModel>
{
new ComboModel
{
CDType = new ComboModelSubModel
{
Name = "Cd-1",
ID = "1",
},
},
new ComboModel
{
CDType = new ComboModelSubModel
{
Name = "Cd-2",
ID = "2",
}
},
new ComboModel
{
CDType = new ComboModelSubModel
{
Name = "Cd-3",
ID = "3",
},
},
new ComboModel
{
CDType = new ComboModelSubModel
{
Name = "Cd-4",
ID = "4",
}
}
};
Assignment = CDTypes.FirstOrDefault();
}
public ObservableCollection<ComboModel> CDTypes
{
get { return _cdTypes; }
set
{
_cdTypes = value;
OnPropertyChanged();
}
}
public ComboModel Assignment
{
get { return _assignment; }
set
{
if (value == null)
_lastAcceptedName = _assignment.CDType.Name;
_assignment = value;
OnPropertyChanged();
}
}
//on lost focus when edit is done will check and update the last edited value
public string AssignmentText
{
get { return _assignmentText; }
set
{
_assignmentText = value;
OnPropertyChanged();
UpDateSourceCollection(AssignmentText);
}
}
//will do the the update on combo lost focus to prevent the
//annessasary updates (each property change will make a lot of noice in combo)
private void UpDateSourceCollection(string assignmentText)
{
var existingModel = CDTypes.FirstOrDefault(model => model.CDType.Name == assignmentText);
if (existingModel != null) return;
if (_lastAcceptedName == null)
{
CDTypes.Add(new ComboModel{CDType = new ComboModelSubModel{ID = string.Empty, Name = assignmentText}});
}
else
{
var existingModelToEdit = CDTypes.FirstOrDefault(model => model.CDType.Name == _lastAcceptedName);
if(existingModelToEdit == null) return;
existingModelToEdit.CDType.Name = assignmentText;
existingModelToEdit.CDType.ID = string.Empty;
}
}
}
public class ComboModel:BaseObservableObject
{
private ComboModelSubModel _cdType;
public ComboModelSubModel CDType
{
get { return _cdType; }
set
{
_cdType = value;
OnPropertyChanged();
}
}
}
public class ComboModelSubModel:BaseObservableObject
{
private string _id;
private string _name;
public string ID
{
get { return _id; }
set
{
_id = value;
OnPropertyChanged();
}
}
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged();
}
}
}
BaseObservableObject code
/// <summary>`enter code here`
/// implements the INotifyPropertyChanged (.net 4.5)
/// </summary>
public class BaseObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> raiser)
{
var propName = ((MemberExpression)raiser.Body).Member.Name;
OnPropertyChanged(propName);
}
protected bool Set<T>(ref T field, T value, [CallerMemberName] string name = null)
{
if (!EqualityComparer<T>.Default.Equals(field, value))
{
field = value;
OnPropertyChanged(name);
return true;
}
return false;
}
}
Regards.

Related

Xamarin.Forms Picker`s ItemDisplayBinding doesn't change displayed text when property on object changes

I have a xaml code:
<Picker ItemsSource="{Binding profiles}" ItemDisplayBinding="{Binding name}" SelectedItem="{Binding selectedProfile}" HorizontalOptions="FillAndExpand" Margin="0, 0, 20, 0" VerticalOptions="Center"/>
In cs file I defined:
BindingContext = this;
and
private ObservableCollection<Profile> _profiles = new ObservableCollection<Profile>();
public ObservableCollection<Profile> profiles
{
get { return _profiles; }
set { _profiles = value; }
}
And profile class is:
public class Profile
{
private string _name = "New profile";
public string name
{
get { return _name; }
set {
_name = value;
}
}
}
It works properly when I add/remove elements, and select new one in dropdown list or code (selectedProfile = profiles[index]).
But the problem occurs when I trying to rename profile. I changed profile name, but Picker didn`t update it and I see the old value.
I also tried this. But now result.
public class Profile : INotifyPropertyChanged
{
private string _name = "New profile";
public string name
{
get { return _name; }
set {
_name = value;
OnPropertyChanged("name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
So, you want to update a information inside a item on a list. Here is my answer to how you can do it:
How to update informations inside a item on a list
You will probably adapt for your need, but I guess that it will help to guide you.
'ItemDisplayBinding' isn't bindable property, that's why I think u can't notify it.
You will have to create a custom picker.
I have faced this problem several times.
Here is a light solution to update a picker linked to a source of type ObservableCollection<T>:
var l_nIndex = Items.IndexOf(SelectedItem);
Items.Move(l_nIndex, l_nIndex);
RaisePropertyChanged(nameof(SelectedItem));
Items property is a collection of Item it is an ObservableCollection<Item>
SelectedItem is the binded property that represents the selected Item in the picker
Here is the xaml code that creates the picker in the view :
<Picker ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />

Binding Combobox Name using Id value

I have a combobox control. I am getting combobox values from Database. There I have Id and Name. But I am binding only Name in the combobox. what i want is,
1. If I select any name in the Combobox, I need to save the Corresponding Id in my Database. Now I am able to bind the value from database and Display the Name in Combobox. but when I select any value in combobox and try to save means, it shows null value on the ID. Here is my code. Please help me to find some solution.
Xaml:
<ComboBox x:Name="cb_rentaltype" HorizontalAlignment="Left" Margin="150,5,0,0" VerticalAlignment="Top" Height="35" Width="200"
SelectedValue="{Binding MasterRentalType}"
DisplayMemberPath="RentalTypeName"
SelectedValuePath="RentalTypeId" />
My code Behind:
var status = new MasterRentalType();
List<MasterRentalType> listRentalType =
status.Get<MasterRentalType>() as
List<MasterRentalType>;
cb_rentaltype.ItemsSource = listRentalType;
and i want to bind the Id to the Data context. here is the code,
private void FacilityDataBind()
{
cb_rentaltype.DataContext = ??
}
Note: MasterRentalType is the Table where i get the Values. There I have ID and Name values.
public class MasterRentalType : EntityBase
{
public string RentalTypeId {get; set;}
public string RentalTypeName {get; set;}
}
how can I bind and save the id value?
Your problem is caused because you have data bound the ComboBox.SelectedValue property to your MasterRentalType property, which I assume is of type MasterRentalType, but then you set the SelectedValuePath property to RentalTypeId. So you're saying *make the SelectedValue use the string RentalTypeId property, but then data binding a MasterRentalType to it.
There are a number of solutions. Correcting your example, you should try this:
<ComboBox x:Name="cb_rentaltype" HorizontalAlignment="Left" Margin="150,5,0,0"
SelectedValue="{Binding MasterRentalType.RentalTypeId}"
DisplayMemberPath="RentalTypeName"
SelectedValuePath="RentalTypeId" />
Alternatively, you could have done this:
<ComboBox x:Name="cb_rentaltype" HorizontalAlignment="Left" Margin="150,5,0,0"
SelectedItem="{Binding MasterRentalType}"
DisplayMemberPath="RentalTypeName" />
To find out more about the differences, please take a look at the How to: Use SelectedValue, SelectedValuePath, and SelectedItem page on MSDN.
There are several ways to achieve this. One of them is implemented below:
The xaml should look like this:
<ComboBox x:Name="cb_rentaltype" HorizontalAlignment="Left" Margin="150,5,0,0"
ItemsSource="{Binding MasterRentalTypeColl, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
SelectedItem="{Binding SelectedMasterRentalType, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
DisplayMemberPath="RentalTypeName">
</ComboBox>
The MasterRentalType class:
public class MasterRentalType : EntityBase, INotifyPropertyChanged
{
private string _RentalTypeId;
private string _RentalTypeName;
public string RentalTypeId
{
get { return _RentalTypeId; }
set
{
_RentalTypeId = value;
NotifyPropertyChanged("RentalTypeId");
}
}
public string RentalTypeName
{
get { return _RentalTypeName; }
set
{
_RentalTypeName = value;
NotifyPropertyChanged("RentalTypeName");
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
The model class:
public class MasterRentalModel: INotifyPropertyChanged
{
private MasterRentalType _SelectedMasterRentalType;
private List<MasterRentalType> _MasterRentalTypeColl = new List<MasterRentalType>();
public MasterRentalType SelectedMasterRentalType
{
get { return _SelectedMasterRentalType; }
set
{
_SelectedMasterRentalType = value;
NotifyPropertyChanged("SelectedMasterRentalType");
}
}
public List<MasterRentalType> MasterRentalTypeColl
{
get { return _MasterRentalTypeColl; }
set { _MasterRentalTypeColl = value; }
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
And in the code-behind, you just need to assign the model to the DataContext of you ComboBox; you can implement this any other function (here I have implemented it in the event handler of Loaded event of my Window):
private void Window_Loaded(object sender, RoutedEventArgs e)
{
MasterRentalModel masterRentalModel = new MasterRentalModel();
// Fill the list of RentalType here
masterRentalModel.MasterRentalTypeColl.Add(new MasterRentalType() { RentalTypeId = "1", RentalTypeName = "Monthly" });
masterRentalModel.MasterRentalTypeColl.Add(new MasterRentalType() { RentalTypeId = "2", RentalTypeName = "Quarterly" });
masterRentalModel.MasterRentalTypeColl.Add(new MasterRentalType() { RentalTypeId = "1", RentalTypeName = "Yearly" });
cb_rentaltype.DataContext = masterRentalModel;
}
Whenever user changes the selection, SelectedMasterRentalType will be updated. And in the SelectedMasterRentalType, you can get both RentalTypeId and RentalTypeName. If you follow proper binding your model will always be updated; that's the essence of WPF.
Hope this will help.

WPF binding to an already bound property

What can I do if I want to bind to some property which is already bound to something else?
In my case, I got a window which has a TextBox. The Text property of this TextBox is data bound to a combo box to it's selectedItem. My Window class got a public string property which I want to update with whatever value is in the TextBox so I wanted to data bind with the Text property but as I said, it's already bound.
How can I update my property with the text in TextBox? Must it use a routed event of TextChanged or can I do it via Xaml?
Also specifically with properties you define them yourself in your window.cs ... how can you bind them to the TextBox.Text? I tried doing it with the <Window> declaration, meaning <Window MyProperty={Binding ...} /> but the property is not recognized there. Why and how do I do it?
You could solve this easily using the MVVM pattern.
ViewModel:
public class ChooseCategoryViewModel : ViewModelBase
{
private string[] _categories =
{ "Fruit", "Meat", "Vegetable", "Cereal" };
public string[] Categories
{
get { return _categories; }
}
private string _selectedCategory;
public string SelectedCategory
{
get { return _selectedCategory; }
set
{
_selectedCategory = value;
OnPropertyChanged("SelectedCategory");
if (value != null && CategoryName != value)
CategoryName = value;
}
}
private string _categoryName;
public string CategoryName
{
get { return _categoryName; }
set
{
_categoryName = value;
OnPropertyChanged("CategoryName");
if (Categories.Contains(value))
{
SelectedCategory = value;
}
else
{
SelectedCategory = null;
}
}
}
}
XAML:
<ComboBox ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}" />
<TextBox Text="{Binding CategoryName}" />

Binding Comboboxes which are in listbox to the viewmodels property(s)

Please look at the image below:
Only three items in the listbox are displayed in the above image but it can be any number of items depending on the user's choice.
Now, as you can see in the image above each item has two comboboxes. Now I want to have selectedItem or SelectedValue in my viewModel from which I should be able to get the user's selection. Now I don't know how to bind these comboboxes for getting the user's selection.
Suppose I have only one item instead of the list then I would declare a property of type int so that I can easily get the selectedValue but for the list I am very much confused. Can anybody point me to the right direction?
To start of, lets say the class you are going to be binding the combo box is
public class UnitSource :INotifyPropertyChanged
{
public IEnumerable Units
{
get { return new[] { "Test Unit", "Alternate Unit" }; }
}
string _selectedComboItem1;
public string SelectedComboItem1
{
get
{
return _selectedComboItem1;
}
set
{
if (_selectedComboItem1 == value)
return;
_selectedComboItem1 = value;
OnPropertyChanged();
}
}
string _selectedComboItem2;
public string SelectedComboItem2
{
get
{
return _selectedComboItem2;
}
set
{
if (_selectedComboItem2 == value)
return;
_selectedComboItem2 = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Then in your view model you will have an ObservableCollection of the UnitSource Like below
public ObservableCollection<UnitSource> MuchoUnitSources
{
get; set;
}
To get the selected ListBoxItem have this in your ViewModel
private UnitSource _selectedUnitSource;
public UnitSource SelectedUnitSource
{
get
{
return _selectedUnitSource;
}
set
{
if (_selectedUnitSource == value)
return;
_selectedUnitSource = value;
OnPropertyChanged();
}
}
Lets assume it is initialized like so
MuchoUnitSources = new ObservableCollection<UnitSource>(new []{ new UnitSource(),new UnitSource() });
The in your view your listbox should look like below
<ListBox Name ="TestList1" ItemsSource="{Binding MuchoUnitSources}" SelectedItem="{Binding SelectedUnitSource}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<ComboBox SelectedItem="{Binding SelectedComboItem1}" ItemsSource="{Binding Units}" />
<ComboBox SelectedItem="{Binding SelectedComboItem2}" ItemsSource="{Binding Units}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Now whenever you select an item from any of the combobox they will update the objectbeing bound to.

Add additional control to ObervableCollection

I'm really new to WPF so apologies in adavnced if this is an obvious question. I have a simple Checkbox in XAML as
<ListBox ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Selections}" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid >
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Path=Item.SelectionName}" />
</Grid >
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Simplified code behind to allow bindings and INotifyPropertyChanged is:
public ObservableCollection<CheckedListItem<Selection>> Selections { get; set; }
public class Selection
{
public String SelectionName { get; set; }
}
Selections = new ObservableCollection<CheckedListItem<Selection>>();
Selections.Add(new CheckedListItem<Selection>(new Selection()
{ SelectionName = "SomeName" }, isChecked: true));
public class CheckedListItem<T> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isChecked;
private T item;
public CheckedListItem()
{ }
public CheckedListItem(T item, bool isChecked = false)
{
this.item = item;
this.isChecked = isChecked;
}
public T Item
{
get { return item; }
set
{
item = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
}
}
public bool IsChecked
{
get { return isChecked; }
set
{
isChecked = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
}
}
}
I now need to add an additional TextBox associated with each Checkbox, so in XAML I have
<ListBox ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Selections}" Margin="12,22,12,94">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid >
<CheckBox IsChecked="{Binding IsChecked}"
Content="{Binding Path=Item.SelectionName}" />
<<TextBox />
</Grid >
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I'm a bit stumped how to include this as part of the ObservableCollection and set it up the binding on both the CheckBox and associated TextBox? Both are being added together using Selections.Add(new CheckedListItem<Selection>(new Selection()
{ SelectionName = "SomeName" }, isChecked: true)); which is causing me some confusion.
EDIT: Added full code
public partial class SelectionSettingWindow : Window
{
public ObservableCollection<CheckedListItem<Selection>> Selections { get; set; }
public class Selection
{
public String SelectionName { get; set; }
public string SelectionTextField { get; set; }
}
public SelectionSettingWindow()
{
InitializeComponent();
Selections = new ObservableCollection<CheckedListItem<Selection>>();
string fg = #"Item1,true,TExtbox1text:Item2,true,TExtbox2text:Item3,false,TExtbox3text"; //Test String
string[] splitSelections = fg.Split(':');
foreach (string item in splitSelections)
{
string[] spSelectionSetting = item.Split(',');
bool bchecked = bool.Parse(spSelectionSetting[1].ToString());
string tbText = spSelectionSetting[2].ToString();
Selections.Add(new CheckedListItem<Selection>(new Selection()
{ SelectionName = spSelectionSetting[0].ToString(),
SelectionTextField = bText }, isChecked: bchecked));
}
DataContext = this;
}
public class CheckedListItem<T> : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isChecked;
private T item;
private string textField;
public CheckedListItem()
{ }
public CheckedListItem(T item, bool isChecked = false)
{
this.item = item;
this.isChecked = isChecked;
}
public T Item
{
get { return item; }
set
{
item = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item"));
}
}
public bool IsChecked
{
get { return isChecked; }
set
{
isChecked = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
}
}
public string TextField
{
get { return textField; }
set
{
textField = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("TextField"));
}
}
}
}
<ListBox ScrollViewer.VerticalScrollBarVisibility="Auto"
ItemsSource="{Binding Selections}" Margin="12,22,12,94">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}"
Content="{Binding Path=Item.SelectionName}" />
<TextBox Text="{Binding Item.SelectionTextField, Mode=TwoWay}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
replace SelectionTextField above with whatever the field is that needs to be edited using the textbox on your Selection class.
Note that I changed the <Grid> to a <StackPanel> So they wouldn't appear on top of eachother and changed the bindings to TwoWay so the changes are reflected in the model.
Make sure your Selection class implements INotifyPropertyChanged (ObservableCollection updates the UI when things get added to/removed from the collection, it doesn't know anything about notifying when it's content's properties change so they need to do that on their own)
Implementing INotifyPropertyChanged on many classes can be cumbersome. I find implementing a base class useful for this. I've got this along with an extra reflection helper for raise property changed available here and a snippet I've made available. It's silverlight but it should work fine for WPF. Using the code I've provided via download you can simply type proprpc and hit tab and visual studio will stub in a property that notifies on change. Some explanation is in one of my old blog posts here and gives credit for where I based the code and snippet from.

Categories

Resources