I'm trying to populate a ComboBox with a pair of String, Value. I did it in code behind like this:
listCombos = new List<ComboBoxItem>();
item = new ComboBoxItem { Text = Cultures.Resources.Off, Value = "Off" };
listCombos.Add(item);
item = new ComboBoxItem { Text = Cultures.Resources.Low, Value = "Low" };
listCombos.Add(item);
item = new ComboBoxItem { Text = Cultures.Resources.Medium, Value = "Medium" };
listCombos.Add(item);
item = new ComboBoxItem { Text = Cultures.Resources.High, Value = "High" };
listCombos.Add(item);
combo.ItemsSource = listCombos;
ComboBoxItem:
public class ComboBoxItem
{
public string Text { get; set; }
public object Value { get; set; }
public override string ToString()
{
return Text;
}
}
As you can see, I'm inserting the Text value using my ResourceDictionary. But if I do it in this way, when I change language at runtime, the ComboBox content doesn't.
So I wanted to try to fill my ComboBox at the design (at XAML).
So my question is: how can I fill my ComboBox with a pair Text, Value like above?
You will use Tag, not Value in xaml.
This would be like this:
<ComboBox>
<ComboBoxItem Tag="L" IsSelected="True">Low</ComboBoxItem>
<ComboBoxItem Tag="H">High</ComboBoxItem>
<ComboBoxItem Tag="M">Medium</ComboBoxItem>
</ComboBox>
Related
I have ComboBoxes in my WPF application that get selected by using the SelectedValue and SelectedValuePath. Sometimes the SelectedValue is not part of the ItemsSource (because the database is inconsistent).
Example:
Items = new ObservableCollection<Item>()
{
new Item() { Id = "1", Text = "Text 1" },
new Item() { Id = "2", Text = "Text 2" },
};
SelectedValue = "3";
When the ComboBox is loaded and no other value is selected, the SelectedValue property is still the inconsistent value ("3" in the example). Is there a way to let the binding automatically clear the SelectedValue when it is not a part of the ItemsSource?
View:
<ComboBox ItemsSource="{Binding Items}"
SelectedValuePath="Id"
SelectedValue="{Binding SelectedValue, Mode=TwoWay}"
DisplayMemberPath="Text" />
ViewModel:
public class MainWindowViewModel : ViewModel
{
public MainWindowViewModel()
{
Items = new ObservableCollection<Item>()
{
new Item() {Id ="1", Text = "Text 1" },
new Item() {Id ="2", Text = "Text 2" },
};
SelectedValue = "3";
}
private ObservableCollection<Item> items;
public ObservableCollection<Item> Items
{
get { return items; }
set { items = value; OnPropertyChanged(); }
}
private string selectedValue;
public string SelectedValue
{
get { return selectedValue; }
set { selectedValue = value; OnPropertyChanged(); }
}
}
public class Item
{
public string Id { get; set; }
public string Text { get; set; }
}
This logic should be implemented in the view model, i.e. you should never set the SelectedValue to 3 when there is no such item in Items. Then you set your view model into an invalid state.
So instead of trying to implement this kind of logic in the view or the control you should implement it in the view model where it belongs. It should be very simple:
SelectedValue = "3";
if (!Items.Any(x => x.Id == SelectedValue))
SelectedValue = null;
Or in the setter:
public string SelectedValue
{
get { return selectedValue; }
set
{
selectedValue = value;
if (!Items.Any(x => x.Id == SelectedValue))
selectedValue = null;
OnPropertyChanged();
}
}
I have a strange use case for WPF DataGrid using MVVM through ReactiveUI that doesn't quite fit any other solution I've found so far.
The Problem Set
I have a DataSet that contains a list of Users. Each User has a string Id and a set of uniquely-identified data fields associated with it that can be represented as a set of string key-value pairs. All Users within a DataSet will have the same set of fields, but different DataSets may have different fields. For example, all Users in one DataSet may have fields "Name", "Age", and "Address"; while Users in another DataSet may have fields "Badge #" and "Job Title".
I would like to present the DataSets in a WPF DataGrid where the columns can be dynamically populated. I would also like to add some metadata to fields that identify what type of data is stored there and display different controls in the DataGrid cells based on that metadata: Pure text fields should use a TextBox, Image filepath fields should have a TextBox to type in a path and a Button to bring up a file-select dialog, etc.
What I Have That Works (but isn't what I want)
I break my data up into ReactiveUI ViewModels. (omitting RaisePropertyChanged() calls for brevity)
public class DataSetViewModel : ReactiveObject
{
public ReactiveList<UserViewModel> Users { get; }
public UserViewModel SelectedUser { get; set; }
};
public class UserViewModel : ReactiveObject
{
public string Id { get; set; }
public ReactiveList<FieldViewModel> Fields { get; }
public class FieldHeader
{
public string Key { get; set; }
public FieldType FType { get; set; } // either Text or Image
}
public ReactiveList<FieldHeader> FieldHeaders { get; }
};
public class FieldViewModel : ReactiveObject
{
public string Value { get; set; } // already knows how to update underlying data when changed
}
I display all of this in a DataSetView. Since Id is always present in Users, I add the first DataGridTextColumn here. Omitting unnecessary XAML for more brevity.
<UserControl x:Class="UserEditor.UI.DataSetView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:UserEditor.UI"
x:Name="DataSetControl">
<DataGrid Name="UserDataGrid"
SelectionMode="Single" AutoGenerateColumns="False"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
DataContext="{Binding Path=ViewModel.Users, ElementName=DataSetControl}">
<DataGrid.Columns>
<DataGridTextColumn Header="Id" Binding="{Binding Id}" MinWidth="60" Width="SizeToCells"/>
</DataGrid.Columns>
</DataGrid>
</UserControl>
And I create additional columns in the code-behind, omitting boiler plate:
public partial class DataSetView : UserControl, IViewFor<DataSetViewModel>
{
// ViewModel DependencyProperty named "ViewModel" declared here
public DataSetView()
{
InitializeComponent();
this.WhenAnyValue(_ => _.ViewModel).BindTo(this, _ => _.DataContext);
this.OneWayBind(ViewModel, vm => vm.Users, v => v.UserDataGrid.ItemsSource);
this.Bind(ViewModel, vm => vm.SelectedUser, v => v.UserDataGrid.SelectedItem);
}
// this gets called when the ViewModel is set, and when I detect fields are added or removed
private void InitHeaders(bool firstInit)
{
// remove all columns except the first, which is reserved for Id
while (UserDataGrid.Columns.Count > 1)
{
UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
}
if (ViewModel == null)
return;
// using all DataGridTextColumns for now
for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
{
DataGridColumn column;
switch (ViewModel.FieldHeaders[i].Type)
{
case DataSet.UserData.Field.FieldType.Text:
column = new DataGridTextColumn
{
Binding = new Binding($"Fields[{i}].Value")
};
break;
case DataSet.UserData.Field.FieldType.Image:
column = new DataGridTextColumn
{
Binding = new Binding($"Fields[{i}].Value")
};
break;
}
column.Header = ViewModel.FieldHeaders[i].Key;
column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;
UserDataGrid.Columns.Add(column);
}
}
When Fields get added or remove, the UserViewModels are updated in DataSetViewModel and InitHeaders is called to recreate the columns. The resulting DataGridCells bind to their respective FieldViewModels and everything works.
What I'm Trying To Do (but doesn't work)
I would like to break FieldViewModel into two derived classes, TextFieldViewModel and ImageFieldViewModel. Each has their respective TextFieldView and ImageFieldView with their own ViewModel dependency property. UserViewModel still contains a ReactiveList. My new InitHeaders() looks like this:
private void InitHeaders(bool firstInit)
{
// remove all columns except the first, which is reserved for Id
while (UserDataGrid.Columns.Count > 1)
{
UserDataGrid.Columns.RemoveAt(UserDataGrid.Columns.Count - 1);
}
if (ViewModel == null)
return;
for (int i = 0; i < ViewModel.FieldHeaders.Count; i++)
{
DataGridTemplateColumn column = new DataGridTemplateColumn();
DataTemplate dataTemplate = new DataTemplate();
switch (ViewModel.FieldHeaders[i].Type)
{
case DataSet.UserData.Field.FieldType.Text:
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(TextFieldView));
factory.SetBinding(TextFieldView.ViewModelProperty,
new Binding($"Fields[{i}]"));
dataTemplate.VisualTree = factory;
dataTemplate.DataType = typeof(TextFieldViewModel);
}
break;
case DataSet.UserData.Field.FieldType.Image:
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(ImageFieldView));
factory.SetBinding(ImageFieldView.ViewModelProperty,
new Binding($"Fields[{i}]"));
dataTemplate.VisualTree = factory;
dataTemplate.DataType = typeof(ImageFieldViewModel);
}
break;
}
column.Header = ViewModel.FieldHeaders[i].Key;
column.Width = firstInit ? DataGridLength.SizeToCells : DataGridLength.SizeToHeader;
column.CellTemplate = dataTemplate;
UserDataGrid.Columns.Add(column);
}
}
The idea is that I create a DataGridTemplateColumn that generates the correct View and then binds the indexed FieldViewModel to the ViewModel dependency property. I have also tried adding a Converter to the Bindings that converts from the base VM to the correct derived type.
The end result is that the DataGrid populates with the correct view, but the DataContext is always a UserViewModel rather than the appropriate FieldViewModel-derived type. The ViewModel is never set, and the VMs don't bind properly. I'm not sure what else I'm missing, and would appreciate any suggestions or insight.
I've figured out an answer that works, though it may not be the best one. Rather than binding to the ViewModel property in my views, I instead bind directly to the DataContext:
factory.SetBinding(DataContextProperty, new Binding($"Fields[{i}]"));
In my views, I add some boilerplate code to listen to the DataContext, set the ViewModel property, and perform my ReactiveUI binding:
public TextFieldView()
{
InitializeComponent();
this.WhenAnyValue(_ => _.DataContext)
.Where(context => context != null)
.Subscribe(context =>
{
// other binding occurs as a result of setting the ViewModel
ViewModel = context as TextFieldViewModel;
});
}
I have a ComboBox that is binded to my ViewModel. The SelectedIndex is binded to a property on the ViewModel.
What I want to do is, with some conditions, some of the choices on the Index becomes invalid so that when the user tries to select it, it should show an error message and not change the currently selected item.
On the back-end, all is well. However, on the UI, the SelectedIndex of the ComboBox still changes. The error message shows properly, but then the 'shown' selected item in the combobox is not the proper one (ex. ComboBox is currently 'Item 4', User selects invalid item 'Item 3', shows error, but the ComboBox still shows 'Item 3').
Here is XAML code for reference:
<ComboBox x:Name="ComboBox_Cover"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Path=Covers}"
SelectedIndex="{Binding Path=Cover,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource Style_ComboBox_CheckSelector}" />
And my ViewModel:
private int _Cover = 0;
public int Cover
{
get { return _Cover; }
set
{
bool canChangeCover = true;
if(IfInvalid())
{
canChangeCover = false;
ShowCoversError();
RaisePropertyChanged("Cover");
}
if(canChangeCover)
{
_Cover = value;
RaisePropertyChanged("Cover");
}
}
}
Am I doing something wrong?
A current workaround I found was using the OnSelectionChanged event and doing setting the SelectedIndex to the proper value there if Invalid. Though I'm not sure if that is a good workaround.
Thank you!
The easiest thing to do would be to take advantage of the IsEnabled property of the ComboBoxItem. Just modify the ItemsSource binding to point to a list of ComboBoxItem.
C#:
public class MainPageViewModel : INotifyPropertyChanged
{
public MainPageViewModel()
{
Covers = new List<ComboBoxItem>
{
new ComboBoxItem { Content = "Item 1", IsEnabled = true },
new ComboBoxItem { Content = "Item 2", IsEnabled = true },
new ComboBoxItem { Content = "Item 3", IsEnabled = true },
new ComboBoxItem { Content = "Item 4", IsEnabled = true }
};
}
public List<ComboBoxItem> Covers { get; set; }
private int selectedIndex;
public int SelectedIndex
{
get { return selectedIndex; }
set
{
if (SelectedIndex != value)
{
foreach (var cover in Covers)
{
if (Covers.IndexOf(cover) < value)
{
cover.IsEnabled = false;
}
}
selectedIndex = value;
NotifyPropertyChanged("SelectedIndex");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(string info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
XAML:
<ComboBox
HorizontalAlignment="Center"
VerticalAlignment="Center"
ItemsSource="{Binding Covers}"
SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" />
If you want to keep a collection of your data models in C#, you could implement an IValueConverter to project a list of Cover into a list of ComboBoxItem. You could also create a new class that inherits from ComboBoxItem and adds some additional dependency properties if you need to bind more values to your control template. For instance, I toyed around with this:
public class CoverComboBoxItem : ComboBoxItem
{
public string Description
{
get { return (string)GetValue(DescriptionProperty); }
set
{
SetValue(DescriptionProperty, value);
this.Content = value;
}
}
public static readonly DependencyProperty DescriptionProperty =
DependencyProperty.Register("Description", typeof(string), typeof(CoverComboBoxItem), new PropertyMetadata(""));
}
It's worth noting that to maintain a nice separation of concerns, I would usually prefer binding a model property directly to the IsEnabled property of the ComboBoxItem. Unfortunately, to make that work you'd need to setup the binding inside of ComboBox.ItemContainerStyle. Unfortunately, declaring bindings there isn't supported. There are some workarounds I've seen, but the complexity of implementing them is a lot more than the solution I've described above.
Hope that helps!
I have a ComboBox binded with an ObservableCollection. How can I do when user enter a text in the ComboBox, if item not in the list, the code automatically add a new item to the list?
<ComboBox Name="cbTypePLC"
Height="22"
ItemsSource="{StaticResource TypePLCList}"
SelectedItem="{Binding TypePLC}" IsReadOnly="False" IsEditable="True">
</ComboBox>
Bind Text property of your combo box to your view model item and then add to the bound collection there, like,
Text="{Binding UserEnteredItem, UpdateSourceTrigger=LostFocus}"
Change the UpdateSourceTrigger to LostFocus because default (PropertyChanged) will communicate each character change to your viewmodel.
// user entered value
private string mUserEnteredItem;
public string UserEnteredItem {
get {
return mUserEnteredItem;
}
set {
if (mUserEnteredItem != value) {
mUserEnteredItem = value;
TypePLCList.Add (mUserEnteredItem);
// maybe you want to set the selected item to user entered value
TypePLC = mUserEnteredItem;
}
}
}
// your selected item
private string mTypePLC;
public string TypePLC {
get {
return mTypePLC;
}
set {
if (mTypePLC != value) {
mTypePLC = value;
// notify change of TypePLC INPC
}
}
}
// your itemsource
public ObservableCollection <string> TypePLCList { set; private set;}
I would like to have a combobox that allows selection from a list of values and also allow a custom value from the typed in text. For display reasons the items are a complex type (lets say the combobox item template displays a patch of color and a flag indicating if it is a custom color).
public class ColorLevel
{
public decimal Intensity { get; set; }
public bool IsCustom { get; set; }
public Color BaseColor { get; set; }
public override ToString() { return string.Format("{0}", Intensity*100); }
}
Example items
var items = new [] {
new ColorLevel { Intensity = 0.9m, IsCustom = false, BaseColor = Color.Red },
new ColorLevel { Intensity = 0.7m, IsCustom = false, BaseColor = Color.Red }
}
XAML
<ComboBox SelectedItem="{Binding Path=SelectedColorLevel}"
IsEditable="true" IsTextSearchEnabled="true">
</ComboBox>
So the above markup works when an item is selected from the item list. And as you type with the text search the matching items are selected. If the typed text doesn't match an item then the SelectedColorLevel is set to null.
The question is at what point (and how) is it best to create a new custom item that can be set to the SelectedColorLevel when the typed text doesn't match an item.
For example I would want to assign a new item to the selected value such as
new ColorLevel { Intensity = decimal.Parse(textvalue), IsCustom = true }
or using an appropriate converter and databinding to the Text property.
Not sure if i fully understood..
You could use the KeyDown event to add a new ColorLevel, for example when Return is pressed.
If items is an ObservableCollection and you set it as the ComboBox's ItemsSource, the new ColorLevel added to items should be available in the list and become the SelectedItem.