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.
Related
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;
});
}
Im using combo box and I have a list that opened when you choose the arrow,I want
to put text on the combo box that you will see it witout opening the combo box,
how can I do that?
currently I try with text="name" ,name="name" witout success ,the combo box doesnt
display anything as a text. just list from drop down...
like the following
http://msdn.microsoft.com/en-us/library/ms753382%28v=vs.85%29.aspx
in addition how can I change the arrow like to be like in the link
Set the first item in the collection list as the default selection.
There is an example here:
How to show text in combobox when no item selected?
EDIT :
public class MyViewModel
{
public MyViewModel()
{
Items.Add("Select one item");
Items.Add("Item1");
Items.Add("Item2");
Items.Add("Item3");
SelectedItem = Items[0];
}
private List<String> _items;
public List<String> Items
{
get{ return _items; }
set
{
_items = value;
RaisePropertyChanged(() => Items);
}
}
private String> _selectedItem;
public String SelectedItem
{
get{ return _selectedItem; }
set
{
_selectedItem= value;
RaisePropertyChanged(() => SelectedItem);
}
}
}
In your xaml file you have to bind to the list of items and to the selected item:
<ComboBox x:Name="myComboBox"
ItemsSource="{Binding Items}"
SelectedValue="{Binding SelectedItem}" />
And don't forget to set the DataContext to your view model.
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>
I have a WPF ComboBox and am using MVVM to bind the ItemsSource and SelectedItem properties. Basically what I want to do is when a user selects a specific item in the combobox, the combobox instead selects a different item.
<ComboBox ItemsSource="{Binding TestComboItemsSource}" SelectedItem="{Binding TestComboItemsSourceSelected}"></ComboBox>
For demo purposes, I also have a button to update the SelectedItem.
<Button Command="{Binding DoStuffCommand}">Do stuff</Button>
I have this in my viewModel:
public ObservableCollection<string> TestComboItemsSource { get; private set; }
public MyConstructor()
{
TestComboItemsSource = new ObservableCollection<string>(new []{ "items", "all", "umbrella", "watch", "coat" });
}
private string _testComboItemsSourceSelected;
public string TestComboItemsSourceSelected
{
get { return _testComboItemsSourceSelected; }
set
{
if (value == "all")
{
TestComboItemsSourceSelected = "items";
return;
}
_testComboItemsSourceSelected = value;
PropertyChanged(this, new PropertyChangedEventArgs(TestComboItemsSourceSelected))
}
}
private ICommand _doStuffCommand;
public ICommand DoStuffCommand
{
get
{
return _doStuffCommand ?? (_doStuffCommand = new RelayCommand(p =>
{
TestComboItemsSourceSelected = "items";
})); }
}
OK, so I want to have the ComboBox select the item "items" whenever the user selects the item "all".
Using the button, I am able to update the combobox's SelectedItem, and I can see this reflected in the UI
I have similar logic to update the viewModel in my setter of the TestComboItemsSourceSelected property. If the user selects "all", instead set the SelectedItem to "items" So code-wise, the viewmodel property gets changed, but this is not reflected in the UI for some reason. Am I missing something? Is there some sort of side-effect of the way I've implemented this?
Well, this is because you change the property while another change is in progress. WPF will not listen to the PropertyChanged event for this property while setting it.
To workaround this, you can "schedule" the new change with the dispatcher, so it will be executed after it is done with the current change:
public string TestComboItemsSourceSelected
{
get { return _testComboItemsSourceSelected; }
set
{
if (value == "all")
{
Application.Current.Dispatcher.BeginInvoke(new Action(() => {
TestComboItemsSourceSelected = "items";
}));
return;
}
_testComboItemsSourceSelected = value;
PropertyChanged(this, new PropertyChangedEventArgs(TestComboItemsSourceSelected))
}
}
The behaviour you are describing seems very weird for me, but if you want a "Select All" feature, the standar way is to create a combobox where items has a CheckBox.
Each item is represented by a small ViewModel (tipically with Id, Name and IsChecked properties), and you manually create a "select all item" that is added first in the ObservableCollection and subscribe to its PropertyChanged in order to set the rest o the items IsChecked property to true.
I have a list box containing many items and I have a combobox in C# WPF.
I would like to bind the itemsource of the combobox to that of the listbox but I want to filter out some of the items (it's an xml datasource, I want to filter by the contents of a certain element of the item element).
Example item:
<Item>
<itemtype>A</itemtype>
<itemname>Name</itemname>
</item>
<Item>
<itemtype>B</itemtype>
<itemname>Name</itemname>
</item>
I thought of manually adding an item to the combobox when adding an item to the listbox but then the 'name' value of the item is not updated when it is changed in the listbox.
What is a good way to do this? Let's say I only want to show all item names with itemtype B. Can it be done in wpf binding or do I have to do some code behind?
It's a reference to the datasource, e.g. to a collection in your viewmodel if you use MVC pattern. The amazing thing is, that it is so simple in usage. The view is updated an has it's own refresh handling. I'll make a little example:
In WPF:
<ListBox ItemsSource={Binding Path=MySource} ItemTemplate="{StaticResource myItemTemplate}" />
The code logic:
public class Item
{
public string Name { get; set; }
public string Type { get; set; }
}
public class MyViewModel
{
public ObservableCollection<Item> MySource { get; set; }
public MyViewModel()
{
this.MySource = new ObservableCollection<Item>();
this.MySource.Add(new Item() { Name = "Item4", Type = "C" });
this.MySource.Add(new Item() { Name = "Item1", Type = "A" });
this.MySource.Add(new Item() { Name = "Item2", Type = "B" });
this.MySource.Add(new Item() { Name = "Item3", Type = "A" });
// get the viewsource
ListCollectionView view = (ListCollectionView)CollectionViewSource
.GetDefaultView(this.MySource);
// first of all sort by type ascending, and then by name descending
view.SortDescriptions.Add(new SortDescription("Type", ListSortDirection.Ascending));
view.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Descending));
// now i like to group the items by type
view.GroupDescriptions.Add(new PropertyGroupDescription("Type"));
// and finally i want to filter all items, which are of type C
// this is done with a Predicate<object>. True means, the item will
// be shown, false means not
view.Filter = (item) =>
{
Item i = item as Item;
if (i.Type != "C")
return true;
else
return false;
};
// if you need a refreshment of the view, because of some items were not updated
view.Refresh();
// if you want to edit a single item or more items and dont want to refresh,
// until all your edits are done you can use the Edit pattern of the view
Item itemToEdit = this.MySource.First();
view.EditItem(itemToEdit);
itemToEdit.Name = "Wonderfull item";
view.CommitEdit();
// of course Refresh/Edit only makes sense in methods/callbacks/setters not
// in this constructor
}
}
Interesting is, that this pattern directly affects the listbox in the gui. If you add the grouping / sorting, this will affect the listbox's display behavior, even if the itemssource is only bound to the viewmodel.
The problem with Binding in this case, is that its only set once. So if you bind it, the sources are synchron. You could use a converter for the binding, which filters the items you not like to bind. Another way would be to use WPF's wonderful CollectionViewSource.
You can add Groupings/Sortings and a Filter to a CollectionViewSource on any kind of ItemsSource. Use the ListCollectionView for example.
ListCollectionView view =
(ListCollectionView)CollectionViewSource.GetDefaultView(yourEnumerableSource);