XAML binding issues with connected comboboxes - c#

I have two comboboxes: Categories and Types. When my form is initially displayed, I am listing all categories and types that exist in the database. For both comboboxes, I manually insert row 0 to be of value “All” so that they don’t have to choose either if they don’t want to.
I have both comboboxes bound to ReactiveObjects so that if the user selects a Category, the Types combobox is automatically re-populated with a query to show only types relevant to the selected Category along with the row 0 added.
When the user selects a Category, it runs the query properly, returns the relevant Types, adds the row 0 properly and the combobox is populated correctly; however, on the XAML size it’s not selecting the row 0, and it adds the red outline around the combobox signifying an invalid selection was made.
If no choice is made for the Type combobox and the form is submitted, the correct value of 0 is passed. So while everything is working properly, the red box around the Types combobox is communicating to the user that they did something wrong and I cannot determine why the XAML isn’t picking up the selected values. I have run the code without it adding the row 0 and it still has the same behavior, i.e., the combobox is populated correctly, but no row is selected and the red outline appears.
XAML for comboboxes
<ComboBox
Grid.Row="3"
Grid.Column="1"
Width="200"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Style="{StaticResource SimpleComboBox}"
ItemsSource="{Binding Categories}"
SelectedValue="{Binding SearchCriteria.CategoryID}"
SelectedValuePath="ComboValueID"
DisplayMemberPath="ComboDataValue"
/>
<TextBlock
Grid.Row="3"
Grid.Column="2"
Style="{StaticResource NormalTextNarrow}"
Text="Type" VerticalAlignment="Top"
/>
<ComboBox
Grid.Row="3"
Grid.Column="3"
Width="200"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Style="{StaticResource SimpleComboBox}"
ItemsSource="{Binding Types}"
SelectedValue="{Binding SearchCriteria.TypeId}"
SelectedValuePath="ComboValueID"
DisplayMemberPath="ComboDataValue"
/>
Relevant VM code
// Definition of SearchCriteria. ResourceItem is a ReactiveObject and
// all of the relevant properties watch for changes in values.
private ResourceItem searchCriteria;
public ResourceItem SearchCriteria
{
get { return searchCriteria; }
set { this.RaiseAndSetIfChanged(ref searchCriteria, value); }
}
// This all happens in my constructor
// Defining Row 0
var b = new GenericCombobox { ComboValueID = 0, ComboDataValue = "All" };
// Populating the comboboxes from the database
Categories = omr.GetKTValues("RES_CATEGORIES");
Types = omr.GetKTValuesRU("RES_TYPES");
// Adding the row 0
Categories.Insert(0, b);
Types.Insert(0, b);
// The form is displayed correctly at this point with the row 0 selected
Problem Code
// When the user picks a category, this is the method that is invoked:
private void categoryChanged()
{
if (SearchCriteria.CategoryID != 0)
{
Types = rr.GetCategoryTypes(SearchCriteria.CategoryID);
SearchCriteria.TypeId = 0;
}
}
// This runs correctly and returns the relevant Types
public List<GenericCombobox> GetCategoryTypes(int categoryId)
{
string sql = "res.usp_GetCategoryTypes";
var types = new List<GenericCombobox>();
SqlConnection sqlCn = DatabaseCommunication.OpenConnection();
using (SqlCommand cmd = new SqlCommand(sql, sqlCn))
{
// Omitting db stuff for brevity...
try
{
SqlDataReader dr = cmd.ExecuteReader();
while (dr.Read())
{
types.Add(new GenericCombobox
{
ComboValueID = (int)dr["TypeId"],
ComboDataValue = (string)dr["Type"],
IsSelected = false,
Description = (string)dr["Type"],
ComboDataCode = (string)dr["Type"]
});
}
// More db-stuff omitted
}
// Adding the row 0
var b = new GenericCombobox { ComboValueID = 0, ComboDataValue = "All", IsSelected = false, Description = "All", ComboDataCode = "All" };
types.Insert(0, b);
return types;
}
Update with Additional Code
// Object containing the TypeId property
public class ResourceItem : ReactiveObject, ISelectable
{
public int Id { get; set; }
public int? OriginalItemId { get; set; }
// ...many other properties...
private int typeId;
public int TypeId
{
get { return typeId; }
set { this.RaiseAndSetIfChanged(ref typeId, value); }
}
// ...and many more follow...

I've been able to reproduce the issue, and I've found a few silly things I can do that make it stop happening.
If I select an item in Types other than "All", then when I change the selection in Category, it selects "All" in Types.
It also works if, in categoryChanged(), I replace this line...
SearchCriteria.TypeId = 0;
with this one:
SearchCriteria = new ResourceItem() {
TypeId = 0,
CategoryID = SearchCriteria.CategoryID
};
If I have ResourceItem.TypeId.set raise PropertyChanged regardless of whether the value has changed or not, it again works correctly.
My hypothesis is that the SelectedItem in the Types combobox (which you aren't even using!) is not changing when the collection changes, because you're not telling it to update SelectedValue.
Setting SearchCriteria.TypeId = 0 is a no-op when SearchCriteria.TypeId is already equal to zero, because RaiseAndSetIfChanged() does just what the name says: It checks to see if the value really changed, and if it hasn't, it doesn't raise PropertyChanged.
SelectedValue happens by chance to be the same value as the new "All" item has, but the combobox doesn't care. It just knows that nobody told it to go find a new SelectedItem, and the old one it has is no good any more because it's not in ItemsSource.
So this works too:
private void categoryChanged()
{
if (SearchCriteria.CategoryID != 0)
{
Types = rr.GetCategoryTypes(SearchCriteria.CategoryID);
SearchCriteria.SelectedType = Types.FirstOrDefault();
//SearchCriteria.TypeId = 0;
//SearchCriteria = new ResourceItem() { TypeId = 0, CategoryID = SearchCriteria.CategoryID };
}
}
XAML:
<ComboBox
Grid.Row="3"
Grid.Column="3"
Width="200"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ItemsSource="{Binding Types}"
SelectedValue="{Binding SearchCriteria.TypeId}"
SelectedItem="{Binding SearchCriteria.SelectedType}"
SelectedValuePath="ComboValueID"
DisplayMemberPath="ComboDataValue"
/>
class ResourceItem
private GenericCombobox selectedType;
public GenericCombobox SelectedType
{
get { return selectedType; }
set { this.RaiseAndSetIfChanged(ref selectedType, value); }
}
I think your best bet is my option #2 above:
private void categoryChanged()
{
if (SearchCriteria.CategoryID != 0)
{
Types = rr.GetCategoryTypes(SearchCriteria.CategoryID);
SearchCriteria = new ResourceItem() {
TypeId = 0,
CategoryID = SearchCriteria.CategoryID
};
// Do you need to do this?
// SearchCriteria.PropertyChanged += SearchCriteria_PropertyChanged;
}
}
The potential problem here is that, in my testing code I called categoryChanged() from a PropertyChanged handler on SearchCriteria. If I create a new SearchCriteria, I need to make sure I handle that event on the new one.
Given that, maybe binding SelectedItem on the Types combobox is the best solution after all: It's the only one I can think of that doesn't require the viewmodel to do strange things to make up for misbehavior in the view that it really shouldn't be aware of.

Related

Binding ComboBox MVVM

This is the first time I try to bind combobox. I'm trying get values from my database. However, with the code below, I'm getting this as a result (the same number of results as the rows count of my table):
GUITest.DB.Structure
where GUITest -> namespace of my project, DB -> folder where structure.cs is.
private ObservableCollection<Structure> _lists;
public ObservableCollection<Structure> Lists
{
get { return _lists; }
set
{
_lists = value;
NotifyOfPropertyChange("Lists");
}
}
public ObservableCollection<Structure> GetStructures()
{
ObservableCollection<Structure> products = new ObservableCollection<Structure>();
using (SqlConnection conn =
new SqlConnection(ConfigurationManager.ConnectionStrings["StringConnexion"].ConnectionString))
{
conn.Open();
SqlCommand cmdNotes =
new SqlCommand("SELECT * FROM Structure", conn);
using (SqlDataReader reader = cmdNotes.ExecuteReader())
{
var ordinals = new
{
CodeStr = reader.GetOrdinal("CODE_STR"),
NomStr = reader.GetOrdinal("NOM_STR"),
};
while (reader.Read())
{
var temp = new TableStructure();
temp.CodeStr = reader.GetString(ordinals.CodeStr);
temp.NomStr = reader.GetString(ordinals.NomStr);
products.Add(temp.SqlProduct2Product());
}
}
}
return products;
}
public CreateAccountViewModel()
{
_lists = new ObservableCollection<Structure>();
Lists = GetStructures();
}
XAML:
<ComboBox SelectedItem="{Binding Path=NomStr}" ItemsSource="{Binding Lists}"></ComboBox>
As noted in the comments, you want DisplayMemberPath not SelectedItem
DisplayMemberPath says "Display this property (as a path) as the ItemTemplate" it is functionally equivalent (though not code equivalent) to, for a path of X:
<ComboBox>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=X}"/>
</DataTemplate>
</ComboBox.ItemTemplate
</ComboBox>
This is why you don't have the Binding extension in it, the framework puts it in for you.
SelectedItem is just that, the current selection of the combo box. It doesn't affect the display in any way.

How to Update ComboBox ItemsSource Without Flickering

I am struggling with an update to a ComboBox that previously worked. I originally had its ItemsSource bound to a read-only ObservableCollection<char> property in the ViewModel. When the user instigates changes (which is done with mouse strokes, so dozens of times per second in some cases), the get rebuilds the collection from the Model and returns it.
When I changed to my own object in the ObservableCollection, the ComboBox started flickering during updates. I'm not sure what's going wrong. Here's the code that works, starting with the XAML:
<ComboBox ItemsSource='{Binding FromBins}' SelectedValue='{Binding SelectedFromBin, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}' />
ViewModel:
public ObservableCollection<char> FromBins
{
get
{
ObservableCollection<char> tempBins = new ObservableCollection<char>();
foreach (var item in Map.BinCounts)
{
tempBins.Add(item.Key);
}
return tempBins;
}
}
I simply raise a property change with every mouse movement and the interface works as expected (there is some other logic to ensure the SelectedItem is valid).
To make the interface more useful, I decided to add more information to the ComboBox, using my own class:
public class BinItem : IEquatable<BinItem>
{
public char Bin { get; set; }
public SolidColorBrush BinColor { get; set; }
public string BinColorToolTip { get {...} }
public BinItem( char bin )
{
Bin = bin;
BinColor = new SolidColorBrush(BinColors.GetBinColor(bin));
}
public bool Equals(BinItem other)
{
return other.Bin == Bin ? true : false;
}
}
If I swap char out for BinItem in the working code ViewModel I get flickering as the mouse is moved. Here is the updated XAML:
<ComboBox ItemsSource='{Binding FromBins}' SelectedValue='{Binding SelectedFromBin, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}'>
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" ToolTip='{Binding BinColorToolTip}'>
<Rectangle Fill='{Binding BinColor}' Width='10' Height='10' HorizontalAlignment='Center' VerticalAlignment='Center' Margin='0,0,4,0' Stroke='#FF747474' />
<TextBlock Text="{Binding Bin}" Width='16' />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
I have tried numerous things, including but not limited to:
-Using a List instead of the ObservableCollection but even though the Get fires every time and returns the correct collection of items, the interface does not always update (though the flickering disappears).
-Leaving all possible bins in the items source and adding a Visibility property to the BinItem class that I bound to (couldn't get it to update).
I suspect I am doing something fundamentally wrong, but no amount of searching SO or otherwise has helped thus far. Any help is appreciated.
I was able to solve this using the ideas from Clemens and Chris. Not sure if this is the most elegant solution, but it works as intended with no measurable performance hit.
Instead of replacing the collection with each refresh, I go through the logic of finding out what's changed (with each update there could be an addition AND a removal simultaneously). Code below:
private ObservableCollection<BinItem> _FromBins = new ObservableCollection<BinItem>();
public ObservableCollection<BinItem> FromBins
{
get
{
if (_FromBins.Count > 0)
{
List<char> BinsToRemove = new List<char>();
foreach (var item in _FromBins)
{
if (!Map.BinCounts.ContainsKey(item.Bin))
{
BinsToRemove.Add(item.Bin);
}
}
foreach (var item in BinsToRemove)
{
_FromBins.Remove(new BinItem(item));
}
}
foreach (var item in Map.BinCounts)
{
if (!_FromBins.Contains(new BinItem(item.Key)) && item.Value > 0) {
_FromBins.Add(new BinItem(item.Key));
}
}
return _FromBins;
}
}
Hope this can help someone else too.

ListBox filled with binding doesn't select item on click

I'm trying to use a ListBox to choose an entry and then display a picture belonging to this selected entry. But just at the beginning I got my first problem: filling the ListBox with binding is working, but if I click on one line in my running program, it doesn't select the line. I can just see the highlighted hover effect, but not select a line. Any ideas what my mistake could be?
This is my XAML:
<ListBox x:Name="entrySelection" ItemsSource="{Binding Path=entryItems}" HorizontalAlignment="Left" Height="335" Margin="428,349,0,0" VerticalAlignment="Top" Width="540" FontSize="24"/>
And in MainWindow.xaml.cs I'm filling the ListBox with entries:
private void fillEntrySelectionListBox()
{
//Fill listBox with entries for active user
DataContext = this;
entryItems = new ObservableCollection<ComboBoxItem>();
foreach (HistoryEntry h in activeUser.History)
{
var cbItem = new ComboBoxItem();
cbItem.Content = h.toString();
entryItems.Add(cbItem);
}
this.entrySelection.ItemsSource = entryItems;
labelEntrySelection.Text = "Einträge für: " + activeUser.Id;
//show image matching the selected entry
if (activeUser.History != null)
{
int index = entrySelection.SelectedIndex;
if (index != -1 && index < activeUser.History.Count)
{
this.entryImage.Source = activeUser.History[index].Image;
}
}
}
So I can see my ListBox correctly filled, but not select anything - so I can't go on with loading the picture matching the selected entry.
I'm still quite new to programming, so any help would be great :)
EDIT: If someone takes a look at this thread later: here's the - quite obvious -solution
XAML now looks like this
<ListBox x:Name="entrySelection" ItemsSource="{Binding Path=entryItems}" HorizontalAlignment="Left" Height="335" Margin="428,349,0,0" VerticalAlignment="Top" Width="540" FontFamily="Siemens sans" FontSize="24">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Code behind to fill it:
//Fill listbox with entries for selected user
DataContext = this;
entryItems = new ObservableCollection<DataItem>();
foreach (HistoryEntry h in selectedUser.History)
{
var lbItem = new DataItem(h.toString());
entryItems.Add(lbItem);
}
this.entrySelection.ItemsSource = entryItems;
labelEntrySelection.Text = "Einträge für: " + selectedUser.Id;
And new Class DataItem:
class DataItem
{
private String text;
public DataItem(String s)
{
text = s;
}
public String Text
{
get
{
return text;
}
}
}
You are filling it with ComboBoxItem, which is not relevant to the ListBox, and also wrong by definition.
You need to have the ObservableCollection filled with data items.
Meaning, make a class that contains the data you want to store, and the ListBox will generate a ListBoxItem automatically per data item.
http://www.wpf-tutorial.com/list-controls/listbox-control/

How to set SelectedId of Combobox and set text of Textbox in a specific format from ViewModel in WPF

I am working on a textbox and combobox in my wpf app. I am sorry for weird title. Well here is the scenario:
Xaml:
<ComboBox ItemsSource="{Binding PortModeList}" SelectedItem="{Binding SelectedPortModeList, Mode=OneWayToSource}" SelectedIndex="0" Name="PortModeCombo" />
<TextBox Grid.Column="1" Text="{Binding OversampleRateBox}" Name="HBFilterOversampleBox" />
ViewModel Class:
public ObservableCollection<string> PortModeList
{
get { return _PortModeList; }
set
{
_PortModeList = value;
OnPropertyChanged("PortModeList");
}
}
private string _selectedPortModeList;
public string SelectedPortModeList
{
get { return _selectedPortModeList; }
set
{
_selectedPortModeList = value;
OnPropertyChanged("SelectedPortModeList");
}
}
private string _OversampleRateBox;
public string OversampleRateBox
{
get
{
return _OversampleRateBox;
}
set
{
_OversampleRateBox = value;
OnPropertyChanged("OversampleRateBox");
}
}
Here I have three requirements:
By using SelectedIdin xaml I am able to select the id but I want to set the selectedid of my combobox from viewmodel class. I.e.
int portorder = 2
PortModeList->SetSelectedId(portOrder). How can I do something like this? or is their any other approach?
I need to restrict number of entries inside a textbox to 4. I.e. 1234 is entered in textbox, it should not let user exceed 4 digits.
I want to set the format of text in OversampleRateBox as: 0x__. I.e. if user wants to enter 23 present in a variable, then I shud set text as 0x23. Basically 0x should be present at the beginning.
Please help :)
I would use the SelectedItem property of the ComboBox (as you are doing), but make the binding two-way. Then, you can set your SelectedPortModeList (which should be called SelectedPortMode) in your view model.
<ComboBox ItemsSource="{Binding PortModeList}"
SelectedItem="{Binding SelectedPortMode}" ...
In the view model:
// Select first port mode
this.SelectedPortMode = this.PortModeList[0];
If you want to limit the number of characters in a TextBox, then use the MaxLength property:
<TextBox MaxLength="4" ... />
If you wish to add a prefix to the OversampleRateBox, one option would be to test for the presence of this in the setter for OversampleRateBox, and if it's not there, then add it before assigning to your private field.
Update
Bind your TextBox to a string property:
<TextBox Grid.Column="1" Text="{Binding OversampleRateBox}" ... />
In the setter for your property, check that the input is valid before setting your private field:
public string OversampleRateBox
{
get
{
return _OversampleRateBox;
}
set
{
// You could use more sophisticated validation here, for example a regular expression
if (value.StartsWith("0x"))
{
_OversampleRateBox = value;
}
// Now if the value entered is not valid, then the text box will be refreshed with the old (valid) value
OnPropertyChanged("OversampleRateBox");
}
}
Because the binding is two-way, you can also set the value from your view model code (for example in the view model constructor):
this.OversampleRateBox = "0x1F";

one combobox dependent on other combobox in WPF

I have this two combobox bound to 2 differnet database table. First is bound to table with 1 column with animal class (example Reptiles, Mammal, Amphibian etc) and the other has two colums Animal names and its respective animal class (example lizard - Reptiles, Snake - Reptiles, Dog - Mammal, Frog- Amphibian). Now what I want is when I select item in combobox1 the combobox2 should have the respective list of animal matching the item selected in combobox 1 with the respective animal class of each animal.
Here is what I have done till now
<ComboBox Grid.Column="2" Grid.Row="4" Margin="3,1,3,1"
Name="comboBox1"
ItemsSource="{Binding Source={StaticResource NameListData}, Path=Animal}"
Selectionchanged="comboBox1_Selectionchanged" />
<ComboBox Grid.Column="2" Grid.Row="5" Margin="3,1,3,1"
Name="comboBox2"
ItemsSource="{Binding Source={StaticResource NameListData}, Path=Stream}" />
The selection_changed function
private static string aa;
private void comboBox1_Selectionchanged(object sender, SelectionchangedEventArgs e)
{
aA = comboBox1.SelectedItem.ToString();
}
public static string aA
{
get { return aa; }
set { aa = value; }
}
The Collections to which 2 comboboxes are bound to and are located in MainViewModel class
DataClasses1DataContext dc = new DataClasses1DataContext();
public List<string> Animal
{
get
{
List<string> facult = new List<string>();
foreach (var a in dc.Faculties)
{
facult.Add(a.Animal1.ToString());
}
return facult;
}
}
string selection;
public List<string> Stream
{
get
{
selection = Schooling.Pages.NewStudentGeneral.aA;
List<string> stream = new List<string>();
var q = from c in dc.Streams where c.faculty ==selection select c;
foreach (var b in q )
{
stream.Add(b.stream1);
}
return stream;
}
}
Here the selection string is not getting the value selected in the combobox1 because whenever i hardcode and specify the selection part like in the statement down below
1 var q = from c in dc.Streams where c.faculty =="Reptiles" select c;
will give the respective animals for the Reptiles.
I guess if the selection variable was getting the selected value of combobox1 my problem have been solved. Or am am doing all wrong here?? please help.
You need to somehow tell your second ComboBox that it needs to update its items, you can either do this through implementations of INotifyPropertyChanged/INotifyCollectionChanged or do it manually. Since you have an explicit event for the selection change in the first ComboBox you might as well update the second manually as well, this could be probably done by changing the handler as follows:
private void comboBox1_Selectionchanged(object sender, SelectionchangedEventArgs e)
{
aA = comboBox1.SelectedItem.ToString();
comboBox2.GetBindingExpression(ComboBox.ItemsSourceProperty).UpdateTarget();
}

Categories