For the purpose of code reuse, I am attempting to bind a ComboBox ItemsSource to an enumerable of enum values defined in a viewmodel. (I am aware of the strategies for binding directly to the enum, but in order to achieve code reuse I need to bind to an enumerable.) On viewmodel construction, I set the selected item to the first value of the enumerable. When the UI first launches, however, the combobox loads with validation error:
Value '' could not be converted.
This error does not occur when I use the same XAML to bind to an enumerable of classes. After I select an enum value, I get no more validation errors and the UI works as intended. How do I avoid this error and get the combobox to display the selected item on startup?
The code details... I have a service implementing IAcquire<T> which returns an enumerable of enum values:
public interface IAcquire<T>
{
IReactiveList<T> Items { get; }
}
My viewmodel inheritance looks something like this:
class GranularitySelectionViewModel : ChartFilterSelectionBase<DataGranularity>
{
public GranularitySelectionViewModel([NotNull] IAcquire<DataGranularity> service)
: base(service, "Granularity")
{}
}
class ChartFilterSelectionBase<T> : SelectionViewModelBase
{
private readonly IAcquire<T> _service;
internal ChartFilterSelectionBase([NotNull] IAcquire<T> service, string label)
:base(label)
{
foreach (var value in service.Items)
{
Items.Add(value);
}
SelectedItem = Items.FirstOrDefault();
}
private readonly IReactiveList<T> _items = new ReactiveList<T>();
public new IReactiveList<T> Items
{
get { return _items; }
}
private T _selectedItem;
public new T SelectedItem
{
get { return _selectedItem; }
set { SetProperty(ref _selectedItem, value); }
}
}
public class SelectionBaseViewModel
{
protected SelectionBaseViewModel([NotNull] string label )
{
if (label == null) throw new ArgumentNullException("label");
_label = label;
}
private readonly string _label;
public string Label
{
get { return _label; }
}
//Placeholder to be overridden in derived class.
public object SelectedItem { get; set; }
//Placeholder to be overridden in derived class.
public IReactiveList<object> Items { get; private set; }
}
The XAML is as follows:
<DataTemplate DataType="{x:Type viewModels:SelectionBaseViewModel}">
<StackPanel Orientation="Vertical">
<Label Content="{Binding Label}" ContentStringFormat="{}{0}:" Margin="5,5,5,0"/>
<ComboBox Margin="5,0,5,5" ItemsSource="{Binding Items, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" IsSynchronizedWithCurrentItem="True"
SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" BorderThickness="1" BorderBrush="White">
</ComboBox>
</StackPanel>
</DataTemplate>
Related
I'm having an issue with my combo box. Somehow it can get out of sync with itself. For example, after I change out my BlockSequenceFields, only the dropdown text gets altered. Below, the Field 1 has been updated but you can see that it doesn't reflect in the currently selected item.
My IsSynchronizedWithCurrentItem=true should make the currently selected item behave as expected but it doesn't seem to work. I've read many stackoverflow posts where the current item doesn't match but they just set IsSynchronizedWithCurrentItem to true and it fixes their issue.
Can anyone explain why this isn't working for me?
<ComboBox x:Name="SequenceFieldComboBox"
SelectedItem="{Binding BlockSequenceFieldIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding BlockSequenceFields, UpdateSourceTrigger=PropertyChanged}"
IsSynchronizedWithCurrentItem="True">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox
IsChecked="{Binding IsCalibrated, Mode=OneWay}"
IsEnabled="False">
</CheckBox>
<TextBlock
Text="{Binding}">
</TextBlock>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
EDIT: Further details for Mr. Chamberlain
// ViewModelBase implements INotifyPropertyChanged
public class BlockFieldViewModel : ViewModelBase
{
public BlockSequenceField SequenceField { get; set; }
public List<BlockSequenceCalibrationItemViewModel> Calibrations => this.SequenceField?.CalibrationList;
public bool IsCalibrated => this.Calibrations.TrueForAll(x => x.IsCalibrated == null || x.IsCalibrated == true);
public double AmplitudeThreshold => this.Calibrations.Max(x => x.Amplitude);
public int FieldNumber { get; set; }
public override string ToString()
{
string ret = string.Format(CultureInfo.CurrentCulture, "Field {0} ", this.FieldNumber);
if (Math.Abs(this.AmplitudeThreshold) > .00001)
{
ret = string.Concat(ret, string.Format(CultureInfo.CurrentCulture, "({0} mA)", this.AmplitudeThreshold));
}
return ret;
}
}
And here is the larger viewmodel, call it MainViewModel.cs. Here are the relevant fields in the class
private ObservableCollection<BlockFieldViewModel> blockSequenceFields;
public ObservableCollection<BlockFieldViewModel> BlockSequenceFields
{
get => this.blockSequenceFields;
set
{
this.blockSequenceFields = value;
this.OnPropertyChanged("BlockSequenceFields");
}
}
private void RefreshFieldList()
{
// In order for the combo box text to update, we need to reload the items
var savedIndex = this.BlockSequenceFieldIndex; // to restore to current field.
var fieldList = this.CalibrationViewModel.FieldViewModels;
this.BlockSequenceFields = new ObservableCollection<BlockFieldViewModel>(fieldList);
this.BlockSequenceFieldIndex = savedIndex;
}
Your problem is caused because BlockFieldViewModel does not raise INPC when FieldNumber is updated. You need to raise it for that property at the minimum.
//Assuming the base class looks like
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public class BlockFieldViewModel : ViewModelBase
{
//...
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
OnPropertyChanged();
}
}
//...
}
I don't know for sure if this will solve your problem or not, due to the fact that you are using .ToString() to display the name. If you find the above does not fix it trigger a property changed for the entire object by passing a empty string in to your OnPropertyChanged method
public int FieldNumber
{
get
{
return _fieldNumber;
}
set
{
if(_fieldNumber.Equals(value))
return;
//Refresh all properties due to the .ToString() not updating.
OnPropertyChanged("");
}
}
Also, if List<BlockSequenceCalibrationItemViewModel> Calibrations can be added to or removed from, or .Amplitude could be changed you need to trigger a refresh of the name from that too.
I've read all over the place, that binding is doable in WPF to Interfaces, but I'm having a heck of a time actually getting any traction with it. I'm using EF Core also, if it helps you ready my code. The ComboBox fills with data, so the bind of the data works, but SelectedItem fails to bind, and the text within the selected item shows blank.
I don't get how the following, binds to the object that implements the interface.
The XAML for ComboBox:
<ComboBox Height="23" x:Name="cbJumpList" Width="177" Margin="2" HorizontalAlignment="Left"
IsEditable="False"
DisplayMemberPath="Name"
SelectedItem="{Binding Path=(model:IData.SelectedJumpList), Mode=TwoWay}"
/>
MainWindow.xaml.cs:
protected IData DB { get; private set; }
public MainWindow()
{
InitializeComponent();
DB = new Data.DataSQLite(true);
DB.Bind_JumpLists_ItemsSource(cbJumpList);
}
IData.cs:
public interface IData : IDisposable, INotifyPropertyChanged
{
void Bind_JumpLists_ItemsSource(ItemsControl control);
IJumpList First_JumpList();
IJumpList SelectedJumpList { get; set; } // TwoWay Binding
}
IJumpList.cs
public interface IJumpList
{
long JumpListId { get; set; }
string Name { get; set; }
}
Then within the implemented object (Data.DataSQLite):
public void Bind_JumpLists_ItemsSource(ItemsControl control)
{
control.ItemsSource = null;
db.JumpLists.ToList();
control.ItemsSource = db.JumpLists.Local;
control.Tag = db.JumpLists.Local;
SelectedJumpList = db.JumpLists.FirstOrDefault();
}
public IJumpList SelectedJumpList
{
get { return _SelectedJumpList; }
set
{
_SelectedJumpList = value;
NotifyPropertyChanged();
}
}
IJumpList _SelectedJumpList;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I should add, the PropertyChanged event remains null.
The SelectedItem property of a ComboBox is supposed to be bound to a property and not to a type. For the binding to work you should also set the DataContext of the ComboBox to an instance of the type where this property is defined.
Try this:
<ComboBox Height="23" x:Name="cbJumpList" Width="177" Margin="2" HorizontalAlignment="Left"
IsEditable="False"
DisplayMemberPath="Name"
SelectedItem="{Binding SelectedJumpList}" />
public void Bind_JumpLists_ItemsSource(ItemsControl control)
{
db.JumpLists.ToList();
control.DataContext = this;
control.ItemsSource = db.JumpLists.Local;
control.Tag = db.JumpLists.Local;
SelectedJumpList = db.JumpLists.FirstOrDefault();
}
So many examples found and none fit! My list box is a list of Result objects. Results can be checked or unchecked in a listbox to mark them as 'Allowed to 'transmit.
<ListBox
x:Name="FileListBox"
ItemsSource="{Binding TestResults}"
ItemTemplate="{StaticResource FileListTemplate}"
SelectionMode="Single"
SelectedItem="{Binding FileListSelected}"
Background="#FFFFFBE2" />
The FileListTemplate
<DataTemplate x:Key="FileListTemplate">
<Grid HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width=".5*" />
<ColumnDefinition Width=".3*" />
<ColumnDefinition Width=".2*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Text="{Binding FileName}" />
<TextBlock Grid.Column="1"
Text="Machine">
</TextBlock>
<CheckBox x:Name="UploadOK"
Grid.Column="2"
HorizontalAlignment="Right"
IsChecked="{Binding CanUpload, Mode=TwoWay}" />
</Grid>
</DataTemplate>
I took out a lot of formatting code to reduce the clutter. So when the check box is checked (or un checked) I need to set a boolean on the object to true or false. But I do not want the ListItem selected just because the checkbox is selected. When the ListItem is selected something else happens. Here is the code for that.
public TestResult FileListSelected
{
get
{
return selectedItem;
}
set
{
if (value == selectedItem)
return;
selectedItem = value;
if (!Workspaces.Any(p => p.DisplayName == value.FileName))
{
this.DisplayTestResult(value as TestResult);
}
base.RaisePropertyChanged("FileListSelected");
}
}
And here is the code I bound to for the Checkbox (although it didn't work).
public bool CanUpload
{
get { return selectedItem.CanUpload; }
set
{
selectedItem.CanUpload = value;
}
}
I appreciate you looking at this.
Internal Class TestResult
{
...
private bool _canUpload;
public bool CanUpload
{
get { return _canUpload; }
set
{
_canUpload = value;
base.RaisePropertyChanged("CanUpload");
}
}
}
When working with MVVM always check for the following:
Add using System.ComponentModel; to your ViewModelClass
Inherit from INotifyPropertyChanged
Always check your DataContext and see the Output Window for BindingErrors
Create Bindings like this:
Example Property:
public string Example
{
get { return _example; }
set
{
_example= value;
OnPropertyChanged();
}
}
this will call OnPropertyChanged automatically every time a new value is assigned (not updated automaticaly once it changes from some other location!)
Make sure your Implementation of INotifyPropertyChanged looks like this:
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
for that you also need using System.Runtime.CompilerServices;
Other options to get your code working:
Your TestResults sould be an ObservableCollection<TestResult>
TestResult should have a property for CanUpload and FileName and inherit from INotifyPropertyChanged
Then on your MainViewModel for example on and ButtonClick your can get the selected files like this:
private List<string> GetSelectedFiles()
{
return TestResults.Where(result => result.CanUpload == true).Select(r => r.FileName).ToList());
}
Note:
FileListSelected is a Property of your ListBox's DataContext which is different to the DataContext of an entry (or at least should be).
FileListSelected will then return the selected Item of your ItemsSource.
Maybe you can comment on this problem with the row selection/checkbox check and add some detail so I can help you more.
EDIT: Notify MainWindowViewModel about CheckBox State Changes:
I see two possible approaches here:
USING EVENT
Add this to your TestResult class:
public delegate void CheckBoxStateChangedHandler(object sender, CheckBoxStateChangedEventArgs e);
public event CheckBoxStateChangedHandler CheckBoxStateChanged;
public class CheckBoxStateChangedEventArgs
{
bool CheckBoxChecked { get; set; }
}
Make sure that on creation of a new TestResult in your MainViewModel you subscribe to that event;
testResult.CheckBoxStateChanged += CheckBox_StateChanged;
Handle what you want to do once the state is changed in CheckBox_StateChanged. Note that the argument e contains the boolean (Checked) and the corresponding TestResult as the sender.
You simply invoke your new Event in the Setter of your CheckBox.Checked Binding:
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
CheckBoxStateChanged.Invoke(this, new CheckBoxStateChangedEventArgs() { CheckBoxChecked = value })
}
}
CALL METHOD ON MAINWINDOWVIEWMODEL
for that you need o create a static object of your MainWindowViewModel (in your MainViewModel) - don't forget to assigne a value once you create your MainWindowViewModel.
public static MainViewModel Instance { get; set; }
then simply add a public Method as you need:
public void CheckBoxValueChanged(bool value, TestResult result)
{
//Do whatever
}
you can also call in from the same spot as the event from above is invoked.
public bool Checked
{
get { return _checked; }
set
{
_checked = value;
OnPropertyChanged();
MainWindowViewModel.Instance.CheckBoxValueChanged(value, this);
}
}
for the past couple of weeks I've been trying to get the EditableTextBlock (from codeproject) working on my TreeView.
The control has a property IsInEditMode which when set to true changes it to a TextBox.
The TreeView is virtualized and declared as follows:
<TreeView x:Name="treeEnvironment"
Margin="0,29,0,0" BorderThickness="0"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
</TreeView>
The TreeView uses the ItemsSource property to get it's data and the value of this is always a single instance of a class (lets call it A). This class contains a list of instances of another type (lets call it B). And this last class contains a list of instances of yet another class (lets call it `C). This is how it looks like in the code:
class A
{
public String Name;
public ObservableCollection<B> Items;
}
class B
{
public String Name;
public ObservableCollection<C> Items;
}
class C
{
public String Name;
public bool IsRenaming;
}
For each of these three classes there is an HierarchicalDataTemplate defined in MainWindow.Resources as follows:
<DataTemplate DataType="{x:Type C}">
<StackPanel Orientation="Horizontal">
<StackPanel.ContextMenu>
<ContextMenu>
<MenuItem Header="Rename" Click="C_Rename_Click" />
</ContextMenu>
</StackPanel.ContextMenu>
<v:EditableTextBlock Text="{Binding Path=Name}" IsInEditMode="{Binding Path=IsRenaming, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</DataTemplate>
<HierarchicalDataTemplate DataType="{x:Type B}" ItemsSource="{Binding Path=Items, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" ToolTip="{Binding Path=Name}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type A}" ItemsSource="{Binding Path=Items, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<Image Source="icons/icon_A.png" Width="16" Height="16" />
<TextBlock Text="{Binding Path=Name}" ToolTip="{Binding Path=Name}" />
</StackPanel>
</HierarchicalDataTemplate>
None of the DataTemplate have keys so that it is applied automatically.
The event that is triggered when the rename MenuItem of C's context menu is clicked, is defined as follows:
private void C_Rename_Click(object sender, RoutedEventArgs e)
{
C instance = (sender as FrameworkElement).DataContext as C;
if (instance != null) {
instance.IsRenaming = true;
} else {
MessageBox.Show("DEBUG: C_Rename_Click(" + sender.ToString() + ", " + e.ToString() + ") : instance == null");
}
}
The problem is that the EditableTextBlock does not turn into a TextBox when the IsRenaming property is set to true on the instance of C that was chosen to be renamed.
The EditableTextBlock works just fine when I place it as a normal control.
My guess is that it has to do with virtualization. Any help would be appreciated.
Thank you for your time, best regards,
100GPing100.
class A, B, C need to implement INotifyPropertyChanged for any changes made to them to be propagated to the UI. You can either implement it in each class individually or have a base class implement INPC and derive your classes from this base class.
Something like:
public class MyBaseViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class A : MyBaseViewModel {
private string _name;
public string Name {
get {
return _name;
}
set {
_name = value;
OnPropertyChanged("Name");
}
}
private ObservableCollection<B> _items;
public ObservableCollection<B> Items {
get {
return _items;
}
set {
_items = value;
OnPropertyChanged("Items");
}
}
}
public class B : MyBaseViewModel {
private string _name;
public string Name {
get {
return _name;
}
set {
_name = value;
OnPropertyChanged("Name");
}
}
private ObservableCollection<C> _items;
public ObservableCollection<C> Items {
get {
return _items;
}
set {
_items = value;
OnPropertyChanged("Items");
}
}
}
public class C : MyBaseViewModel {
private string _name;
public string Name {
get {
return _name;
}
set {
_name = value;
OnPropertyChanged("Name");
}
}
private bool _isRenaming;
public bool IsRenaming {
get {
return _isRenaming;
}
set {
_isRenaming = value;
OnPropertyChanged("IsRenaming");
}
}
}
Now when you change IsRenaming in your code, you will see the update propagate to the UI and the TextBlock switch to a TextBox.
Side-note
Please have a look at MVVM. If you're not sure about it. Get to learn it slowly cos it helps UI development in WPF quite a bit.
How can I get the text of a RadAutoCompleteBox using RadControls Q1 2013 in C#?
autoCompleteBox.SelectedItem returns "ServerCrafterTelerikWPF.Command".
Edit 1:
Here's my XAML:
<telerik:RadAutoCompleteBox x:Name="txtboxCommand" ItemsSource="{Binding Commands, Source={StaticResource ViewModel}}"
DisplayMemberPath="ACommand" AutoCompleteMode="Append" HorizontalAlignment="Left"
telerik:StyleManager.Theme="Modern" Margin="280,405,0,0"
VerticalAlignment="Top" Width="330" Height="30" KeyDown="txtboxCommand_KeyDown"/>
And I don't have any C# code. I just want, when a button is pressed, to get the text that is in the RadAutoCompleteBox.
Edit 2:
And here's my collection:
public class Command
{
public string ACommand { get; set; }
}
/// <summary>
/// A view model for MainWindow.xaml
/// </summary>
public class ViewModel
{
public ObservableCollection<Command> Commands { get; set; }
public ViewModel()
{
Commands = new ObservableCollection<Command>()
{
new Command() {ACommand = "stop "},
// Other commands...
// ...
// ...
};
}
}
You should take it from the SelectedItem property. Cast it to your class and then get it from MyClass.ACommand
And I suggest binding SelectedItem with Mode=TwoWay in your ViewModel can help a lot.
Just add a Member to ViewModel which is implementing Command like:
private Command _SelectedItem;
public Command SelectedItem
{
//get set with INotifyPropertyChanged
}
Then from the xaml: Bind RadAutoCompleteBox's SelectedItem Property like:
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
I have reproduced the problem.
Yes. I had the same problem. And I found the problem and the answer too.
I got the problem because of using of type string for the selected item in my view model.
private string selectedCommand;
public string SelectedCommand
{
get
{
return selectedCommand;
}
set
{
selectedCommand = value;
NotifyPropertyChanged("SelectedCommand");
}
}
Use the type as Command class and your problem will be solved.
private Command selectedCommand;
public Command SelectedCommand
{
get
{
return selectedCommand;
}
set
{
selectedCommand = value;
NotifyPropertyChanged("SelectedCommand");
}
}
Bind the SelectedItem property of the RadAutoCompleteBox in the XAML
<telerik:RadAutoCompleteBox
x:Name="txtboxCommand"
ItemsSource="{Binding Commands, Source={StaticResource ViewModel}}"
DisplayMemberPath="ACommand"
AutoCompleteMode="Append"
HorizontalAlignment="Left"
telerik:StyleManager.Theme="Modern"
Margin="280,405,0,0"
VerticalAlignment="Top"
Width="330"
Height="30"
KeyDown="txtboxCommand_KeyDown"
SelectedItem="{Binding SelectedCommand, Mode=TwoWay}"/>
If you wanna get the selected item by the code-behind, convert the selected item to the Command class type.
var selectedItem = autoCompleteBox.SelectedItem as Command;
And actually there can be multiple selected items. In that case you have to define a collection of Command objects.
private ObservableCollection<Command> selectedCommands;
public ObservableCollection<Command> SelectedCommands
{
get
{
return selectedCommands;
}
set
{
selectedCommands = value;
NotifyPropertyChanged("SelectedCommands");
}
}
And bind it to the SelectedItems property (plural of SelectedItem) of the RadAutoCompleteBox control.
SelectedItems="{Binding SelectedCommands, Mode=TwoWay}"
And make sure you have initiated the SelectedItems.
this.SelectedCommands = new ObservableCollection<Command>();
The SearchText property of the RadAutoCompleteBox should provide you the value.
According to the documentation it gets or sets the string that is into the TextBox part of the RadAutoCompleteBox. The SearchText value is used to filter the RadAutoCompleteBox' ItemsSource.
If you want to get the "Text" of the selected item of the AutocompleteBox, then you need to cast it to the specified type. In your case it is of type ServerCrafterTelerikWPF.Command.
var selectedItem = autoCompleteBox.SelectedItem;
if (selectedItem is ServerCrafterTelerikWPF.Command) {
var selectedCommand = selectedItem as ServerCrafterTelerikWPF.Command;
string textOfAutoCompleteBox = selectedCommand.ACommand;
}