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...
Related
So let me preface by saying that I am very new to WPF and MVVM.
I am using the mvvm design pattern for my application. My goal, is that I need to have two combo boxes loaded with content to select from( in this case, units to convert from and to). The content of these combo boxes is determined by a third combo box which determines the type of units to load.
So for example, the first combo box would let the user select a unit type, such as speed or temperature. So if I select temperature, the other two combo boxes would be loaded with a list of temperature units. Likewise if I select speed, then the list in the other two combo boxes would be replaced with units for speed.
I already have a class that handles the from and to conversion. But I'm a little lost with how to start working with these combo boxes. I have only done some basic things with combo boxes like loading content straight in the xaml. I have seen people make lists and somehow bind them but some it was a little overwhelming.
All I need is a good example and explanation to get me started. Would greatly appreciate it.
Everything you need is a ViewModel class to work with the binding.
Each combo box will binding the ItemSources to a Property in the ViewModel. Everytime the selected of the first combo box is change, you will update the data source of the second combo box.
Here is example of the ViewModel class:
namespace WpfApp1
{
class SampleVM : ViewModelBase
{
private ObservableCollection<UnitEntry> _comboBox1ItemSource;
private ObservableCollection<TypeEntry> _comboBoxTypeItemSource;
private int _selectedTypeIndex;
public ObservableCollection<UnitEntry> ComboBoxUnitItemSource
{
get => _comboBox1ItemSource;
set
{
_comboBox1ItemSource = value;
RaisePropertyChange(nameof(ComboBoxUnitItemSource));
}
}
public ObservableCollection<TypeEntry> ComboBoxTypeItemSource
{
get => _comboBoxTypeItemSource;
set
{
_comboBoxTypeItemSource = value;
RaisePropertyChange(nameof(ComboBoxTypeItemSource));
}
}
public int SelectedTypeIndex
{
get => _selectedTypeIndex;
set
{
_selectedTypeIndex = value;
RaisePropertyChange(nameof(SelectedTypeIndex));
//Here where we will handle the data in the second combo box depend on the Type value when it changed
if(value == 0)
{
ComboBoxUnitItemSource = GetDataUnitType1();
}
else
{
ComboBoxUnitItemSource = GetDataUnitType2();
}
}
}
public SampleVM()
{
InitData();
}
private void InitData()
{
//Init Type data
ComboBoxTypeItemSource = new ObservableCollection<TypeEntry>();
TypeEntry type1 = new TypeEntry(0, "Type 1");
TypeEntry type2 = new TypeEntry(1, "Type 2");
ComboBoxTypeItemSource.Add(type1);
ComboBoxTypeItemSource.Add(type2);
//Selected Index set to default by 0
SelectedTypeIndex = 0;
}
private ObservableCollection<UnitEntry> GetDataUnitType1()
{
//Get your real data instead of fake data below
ObservableCollection<UnitEntry> data = new ObservableCollection<UnitEntry>();
for (int i = 0; i < 5; i++)
{
UnitEntry unitEntry = new UnitEntry(i, $"Type 1 - Entry: {i}");
data.Add(unitEntry);
}
return data;
}
private ObservableCollection<UnitEntry> GetDataUnitType2()
{
//Get your real data instead of fake data below
ObservableCollection<UnitEntry> data = new ObservableCollection<UnitEntry>();
for (int i = 0; i < 5; i++)
{
UnitEntry unitEntry = new UnitEntry(i, $"Type 2 - Entry: {i}");
data.Add(unitEntry);
}
return data;
}
}
public class TypeEntry
{
public int ID { get; set; }
public string Name { get; set; }
public TypeEntry(int id, string name)
{
ID = id;
Name = name;
}
}
public class UnitEntry
{
public int ID { get; set; }
public string Name { get; set; }
public UnitEntry(int id, string name)
{
ID = id;
Name = name;
}
}
}
And here is the xaml class looks like:
<!-- The "Name" value is the Name property in the Entry class-->
<ComboBox Grid.Row="0"
Grid.Column="0"
Width="200"
Height="30"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedIndex="{Binding SelectedTypeIndex}"
ItemsSource="{Binding ComboBoxTypeItemSource}"/>
<ComboBox Grid.Row="0"
Grid.Column="1"
Width="200"
Height="30"
DisplayMemberPath="Name"
SelectedValuePath="Name"
SelectedIndex="0"
ItemsSource="{Binding ComboBoxUnitItemSource}"/>
Finally, important part, you need to assign the ViewModel to the View class:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new SampleVM();
}
}
I have a search bar with the property Text binded to a string property in my ViewModel.
I also have Behaviors within the search bar so that every time the text is changed a search is done within a list of objects using NewTextValue passed to as the query string.
The issue I have is that, I make the ListView invisible until a non-empty string is passed to my Search/Filter command (obviously.. :)). I have tried to enforcing hiding the ListView for a couple scenarios e.g. if all text is removed from the search bar.
When an item is selected from the now visible list view I used that item to populate the Text property of my SearchBar, after which I cannot hide it within code. All attempts have failed and the ListView remains visible. Note: I explicity created a hide button separately and saw it worked so I am wondering if I cannot tie hiding the view with setting the searchbar Text property.
View
<SearchBar Text="{Binding SearchText}">
<SearchBar.Behaviors>
<prismBehaviors:EventToCommandBehavior EventName="TextChanged"
Command="{Binding FilterOccupationsListCommand}"
EventArgsParameterPath="NewTextValue"/>
</SearchBar.Behaviors>
</SearchBar>
<ListView ItemsSource="{Binding FilteredOccupations}" IsVisible="{Binding FilteredOccupationsVisible}" SelectedItem="{Binding Occupation, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<TextCell Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Please Note : My ViewModel inherits from BaseViewModel which inherits INotifyPropertyChanged. SetProperty() is what notifies the property. This is quite common with MvvmCross, Prism etc.
ViewModel
public class MyViewModel : BaseViewModel
{
public DelegateCommand<string> FilterOccupationsListCommand { get; }
public MyViewModel()
{
FilterOccupationsListCommand = new DelegateCommand<string>(FilterOccupationsList);
}
private void FilterOccupationsList(string query)
{
if (!string.IsNullOrWhiteSpace(query))
{
FilteredOccupationsVisible = true;
var searchResult = Occupations.Where(x => x.Name.ToLower().Contains(query));
FilteredOccupations = new ObservableCollection<Occupation>(searchResult);
}
else
FilteredOccupationsVisible = false;
}
private Occupation _occupation;
public Occupation Occupation
{
get => _occupation;
set
{
SetProperty(ref _occupation, value);
SearchText = value.Name;
}
}
private string _name;
public string Name { get => _name; set => SetProperty(ref _name, value); }
private string _searchText;
public string SearchText
{
get => _searchText;
set {
SetProperty(ref _searchText, value);
FilteredOccupationsVisible = false;
}
}
private bool _filteredOccupationsVisible;
public bool FilteredOccupationsVisible { get => _filteredOccupationsVisible; set => SetProperty(ref _filteredOccupationsVisible, value); }
public ObservableCollection<Occupation> _filteredOccupations = new ObservableCollection<Occupation>();
public ObservableCollection<Occupation> FilteredOccupations { get => _filteredOccupations; set { SetProperty(ref _filteredOccupations, value); } }
}
If not using Behaviors in SearchBar , you can have a try with TextChanged method of itself.
<SearchBar x:Name="MySearchBar" Text="SearchText" TextChanged="SearchBar_TextChanged" />
In ContentPage , when text cheanged fire here :
MyViewModel myViewModel = new MyViewModel();
private void SearchBar_TextChanged(object sender, TextChangedEventArgs e)
{
Console.WriteLine("new -- " + e.NewTextValue + "-- old -- " + e.OldTextValue);
Console.WriteLine("MyEntry --" + MySearchBar.Text);
//Here can invoke FilterOccupationsList of MyViewModel
myViewModel.FilterOccupationsList(MySearchBar.Text);
}
Else if using Command to do , you need to add isntance of ICommand in MyViewModel to invoke FilterOccupationsList.
public class MyViewModel : BaseViewModel
{
public ICommand FilterOccupationsListCommand { private set; get; }
...
public MyViewModel()
{
FilterOccupationsListCommand = new Command<string>((NewTextValue) =>
{
// Pass value to FilterOccupationsList.
Console.WriteLine("SearchBar new text --" + NewTextValue);
FilterOccupationsList(NewTextValue);
});
}
...
}
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 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 have got a DataGridComboBoxColumn that I need to translate with the WPFLocalizationExtension.
I have got a static method in my view model, that provides the available values:
public static List<ProfileSegmentType> AvailableSegmentTypes
{
get
{
var availableSegmentTypes = new List<ProfileSegmentType>
{
new ProfileSegmentType { Value = ProfileSegmentTypeEnum.Arc },
new ProfileSegmentType { Value = ProfileSegmentTypeEnum.Line },
};
return availableSegmentTypes;
}
}
The ProfileSegmentType looks like this:
public class ProfileSegmentType
{
public ProfileSegmentTypeEnum Value { get; set; }
public string Label
{
get { return Resources.Localization.ADummyValue; }
}
}
My data grid column definition looks like this:
<DataGridComboBoxColumn Header="..."
ItemsSource="{Binding Source={x:Static viewModels:ProfileGeometryViewModel.AvailableSegmentTypes}}"
SelectedValueBinding="{Binding SegmentType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedValuePath="Value"
DisplayMemberPath="Label" />
Now I get labels translated (right now the dummy value). But when I switch the language, the cells are not updated, of course. How can I achieve is, that the cell contents are updated, when I switch the language? (I also tried to use a value converter, but couldn't get it to work this way)
With the help of your comments, I could solve it. I had to change my ProfileSegmentType like so:
public class ProfileSegmentType : INotifyPropertyChanged
{
public ProfileSegmentType()
{
LocalizeDictionary.Instance.PropertyChanged += (e, a) =>
{
if (a.PropertyName == "Culture")
PropertyChanged(this, new PropertyChangedEventArgs("Label"));
};
}
public ProfileSegmentTypeEnum Value { get; set; }
public string Label
{
get { return Common.Resources.Localization.Add; }
}
public event PropertyChangedEventHandler PropertyChanged = (e, a) => {};
}