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!
Related
Please help me on this issue in WPF using MVVM
I Have a drop down ,In that 5 values were there. Onload the drop down needs to be displayed only enabling the first value and remaining needs to be disable.Once i click/check the first check box under the drop down it needs to be enable all other disabled check boxes values and also the selected value from the drop-down needs to displayed in the same combo box(multiselect) separated by comma or any delimiters using wpf MVVM architecture.
Here's something that approximately does what you want. Define a view model for the individual items in the drop down (ComboBox):
public class ChoiceViewModel : ViewModelBase
{
private string _content;
public string Content
{
get => _content;
set => Set(ref _content, value);
}
private bool _isChecked;
public bool IsChecked
{
get => _isChecked;
set => Set(ref _isChecked, value);
}
private bool _isEnabled;
public bool IsEnabled
{
get => _isEnabled;
set => Set(ref _isEnabled, value);
}
}
The ViewModelBase class comes from the MVVM Light Toolkit. It defines the Set method I used above, which raises the INotifyPropertyChanged event whenever the property is changed.
Next, define a view model to contain the list of choices:
public class ViewModel : ViewModelBase
{
public ViewModel()
{
Choices = new ObservableCollection<ChoiceViewModel>
{
new ChoiceViewModel {Content = "Check to enable the others", IsEnabled = true},
new ChoiceViewModel {Content = "Choice 1", IsEnabled = false},
new ChoiceViewModel {Content = "Choice 2", IsEnabled = false},
new ChoiceViewModel {Content = "Choice 3", IsEnabled = false},
new ChoiceViewModel {Content = "Choice 4", IsEnabled = false},
};
Choices[0].PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(ChoiceViewModel.IsChecked))
{
var choice = (ChoiceViewModel)sender;
for (var i = 1; i < Choices.Count; i++)
Choices[i].IsEnabled = choice.IsChecked;
RaisePropertyChanged(nameof(ChoicesDisplay));
}
};
for (var i = 1; i < Choices.Count; i++)
{
Choices[i].PropertyChanged += (sender, args) =>
{
if (args.PropertyName == nameof(ChoiceViewModel.IsChecked))
RaisePropertyChanged(nameof(ChoicesDisplay));
};
}
}
public ObservableCollection<ChoiceViewModel> Choices { get; }
public string ChoicesDisplay =>
CheckedChoices.Any()
? string.Join(", ", CheckedChoices.Select(x => x.Content))
: "No choices made";
private IEnumerable<ChoiceViewModel> CheckedChoices =>
Choices.Skip(1).Where(x => x.IsChecked);
}
The view model handles enabling or disabling the other choices when the first choice is checked or unchecked. It also handles updating the display for the choices selected, separated by commas.
In the Window class (or whatever is appropriate for your situation), set the DataContext to the ViewModel:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
The XAML looks like this:
<ComboBox
ItemsSource="{Binding Choices}"
Text="{Binding ChoicesDisplay, Mode=OneWay}"
IsEditable="True"
IsReadOnly="True"
>
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox
Content="{Binding Content}"
IsChecked="{Binding IsChecked}"
IsEnabled="{Binding IsEnabled}"
/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
This solution does what you want, but there are some things you may want to fix. For example, even though a check box is disabled (and therefore you can't check or uncheck it), you can still choose it in the drop down.
I have a window with a combobox. This comboboxhas 5 ComboboxItems.
In the example I want that it is not possible to select the items 3, 4 and 5.
I've tried two different ways: MVVM way and codebehind way
MVVM way:
xaml:
<ComboBox SelectedIndex="{Binding Path=SaveIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Path=SaveSelectedItemCheck}" Name="SaveCombobox">
viewmodel:
public object SaveSelectedItemCheck
{
get { return _control.SaveCombobox.Items[CurrentSaveIndex]; }
set
{
if (value != _control.SaveCombobox.Items[0] && value != _control.SaveCombobox.Items[1])
{
OnPropertyChanged("SaveSelectedItemCheck");
}
}
}
codebehind way:
xaml:
<ComboBox SelectedIndex="{Binding Path=SaveIndex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectionChanged="Save_SelectionChanged">
codebehind:
private void Save_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox combobox = sender as ComboBox;
if(combobox == null)
{
return;
}
if (combobox.SelectedItem != combobox.Items[0] && combobox.SelectedItem != combobox.Items[1])
{
combobox.SelectedItem = combobox.Items[1];
e.Handled = true;
}
}
But it only works with the codebehind way, which is dirty.
Why doesn't work the MVVM way?
As others said, you do not actually set any value in the property setter.
But more important IMO, I think you've misunderstood the MVVM key concepts. There are lots of issues with your ViewModel code:
public object SaveSelectedItemCheck
{
get { return _control.SaveCombobox.Items[CurrentSaveIndex]; }
set
{
if (value != _control.SaveCombobox.Items[0] && value != _control.SaveCombobox.Items[1])
{
OnPropertyChanged("SaveSelectedItemCheck");
}
}
}
You're referring to _control.SaveCombobox.Items, which are UI concept/objects. This isn't the goal of the ViewModel. And you're returning an object, you should strongly type your model!
What you should have is the following:
a model (strongly typed POCO classes)
ViewModels that do not deal with the view controls in any way (you could even separate views and ViewModels into different assemblies to ensure you're following this rule)
Views, with binding to ItemsSource for control such as Combobox
Model:
public class SomeObject : INotifyPropertyChanged
{
private string someProperty;
public string SomeProperty
{
get { return this.someProperty; }
set
{
if (this.someProperty != value)
{
this.someProperty = value;
OnPropertyChanged("SomeProperty");
}
}
}
...
}
ViewModel:
public class ViewModel : SomeViewModelBase
{
private ObservableCollection<SomeObject> items;
private SomeObject selectedItem;
public ObservableCollection<SomeObject> Items
{
get
{
return items;
}
set
{
if (this.items != value)
{
this.items = value;
OnPropertyChanged("Items");
}
}
}
public ObservableCollection<SomeObject> SelectedItem
{
get
{
return selectedItem;
}
set
{
if (this.selectedItem != value)
{
this.selectedItem = value;
OnPropertyChanged("SelectedItem");
}
}
}
...
// Anywhere in your view model:
this.Items = new ObservableCollection<SomeObject>(...);
this.SelectedItem = this.Items[2];
// Etc.
}
View:
<ComboBox
ItemsSource={Binding Items}
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
View code-behind:
Nothing for your example
Your ViewModel method doesn't set the value of the property - regardless of whether the value is valid or not. It just fires an event based on whether the value is valid.
In fact, on closer inspection you appear to have misunderstood the MVVM pattern somewhat, as it appears that your ViewModel code might be referring directly to the control it is supporting. You should have a backing field for your property as per a "normal" property.
More importantly, you should throw the PropertyChanged event whether the value is valid or not, because if the value has been overriden by the viewmodel then PropertyChanged will notify the UI that the combobox value needs to be re-set to a valid value.
You don't store any value in the setter in your MVVM way.
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.
Tried may approches to displaying a "no data" if there are no items in listbox. Since I'm on wp7 and using silverlight I can't use DataTriggers, so I've created a control to have it behave consistently across the whole app. BUT I if you set the breakpoint for the set method - it's not being called at all!
The control class
public class EmptyListBox : ListBox
{
public new IEnumerable ItemsSource
{
get
{
return base.ItemsSource;
}
set
{
// never here
base.ItemsSource = value;
ItemsSourceChanged();
}
}
protected virtual void ItemsSourceChanged()
{
bool noItems = Items.Count == 0;
if (noItems)
{
if (Parent is System.Windows.Controls.Panel)
{
var p = Parent as Panel;
TextBlock noData = new TextBlock();
noData.Text = "No data";
noData.HorizontalAlignment = HorizontalAlignment;
noData.Width = Width;
noData.Height = Height;
noData.Margin = Margin;
p.Children.Add(noData);
Visibility = System.Windows.Visibility.Collapsed;
}
}
}
}
This is xaml
<my:EmptyListBox ItemsSource="{Binding Path=MyData}" Name="myListBox">
<my:EmptyListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=name}" />
</DataTemplate>
</my:EmptyListBox.ItemTemplate>
</my:EmptyListBox>
Codebehind:
ClientModel ClientInfo { get; set; }
public ClientView()
{
ClientInfo = new ClientModel();
ClientInfo.PropertyChanged += new System.ComponentModel.PropertyChangedEventHandler(DataReady);
DataContext = ClientInfo
}
ClientModel class:
public class ClientModel : INotifyPropertyChanged
{
MyData _myData;
public MyData MyData
{
get
{
return _myData;
}
set
{
_myData = value;
NotifyPropertyChanged("MyData");
}
}
public void GetClient(int id)
{
// fetch the network for data
}
}
LINK TO SOLUTION .ZIP THAT SHOWS THE PROBLEM
http://rapidshare.com/files/455900509/WindowsPhoneDataBoundApplication1.zip
Your new ItemSource should be a DependencyProperty.
Anything that is working with Bindings have to be a DependencyProperty.
Simply make it a DependencyProperty.
I think the solution I'd go for is something like this:
Define a new visual state group ItemsStates and two visual states: NoItems and HasItems.
In the ControlTemplate for your custom listbox, add the visual tree for your "no data" state.
In the NoItems state, set the Visibility of your "no data" elements to Visible and set the Visibility of the default ItemsPresenter to Collapsed.
In the HasItems state, swap the Visibility of these elements.
In an OnApplyTemplate override switch to the Empty state by default: VisualStateManager.GoToState(this, "Empty", true);
In an OnItemsChanged override, check whether the items source is empty and use VisualStateManager to switch between these states accordingly.
That should work :)
Create ItemsSource as a DependencyProperty.
Example:
public IEnumerable ItemsSource
{
get { return (IEnumerable)base.GetValue(ItemsSourceProperty); }
set { base.SetValue(ItemsSourceProperty, value); }
}
public static DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(
"ItemsSource",
typeof(IEnumerable),
typeof(EmptyListBox),
new PropertyMetadata(null));
try to implement the INotifyPropertyChanged interface and use for ItemsSource an ObservableCollection. In the Setter of your Property just call the OnPropertyChanged method.
Maybe this will help.
Try adding Mode=TwoWay to the ItemsSource binding:
<my:EmptyListBox ItemsSource="{Binding Path=MyData, Mode=TwoWay}" Name="myListBox">
<my:EmptyListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=name}" />
</DataTemplate>
</my:EmptyListBox.ItemTemplate>
</my:EmptyListBox>
I have a combo box defined as such
<ComboBox Name="RoomDropDown" Visibility="{Binding Path=RoomDropDownVisible,Mode=OneWay,Converter={StaticResource BoolVisibilityConvertor}}"
ItemsSource="{Binding Path=RoomList,Mode=OneWay}" DisplayMemberPath="display" SelectedValuePath="display" SelectedValue="{Binding Path=Room,Mode=TwoWay}"/>
There are 2 properties defined in the ViewModel, RoomList which is List and the Room property which is a string.
First time when i run the app everything works fine, and the Drop Down gets the correct values as well as the correct values is selected. However on a certain conditions the RoomList property is changed to a different source & the Room property is also changed. The problem that is now happening is the Combo Box is showing the correct values but the selected value is not getting selected. Worse, we can live with that, but the setter is also not firing when the value is manually changed in the DropDown.
Any pointers on what is going wrong here?
Followup:
Don't think I managed to get the exact problem across, here is some sample code that I wanted to add to illustrate the problem:
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel VerticalAlignment="Center" Width="100">
<ComboBox Name="TestBox" Height="20" Width="100" ItemsSource="{Binding Path=ComboSource}" DisplayMemberPath="display" SelectedValuePath="code"
SelectedValue="{Binding Path=ComboSelection,Mode=TwoWay}"/>
<Button Content="Click Here" Click="Button_Click" />
</StackPanel>
</Grid>
public MainPage()
{
InitializeComponent();
this.Loaded += (s, e) =>
{
var temp = new List<Binding>();
temp.Add(new Binding() { code = "1", display = "One" });
temp.Add(new Binding() { code = "2", display = "Two" });
this.ComboSource = temp;
this.ComboSelection = "1";
this.DataContext = this;
};
}
private static readonly DependencyProperty ComboSelectionProperty =
DependencyProperty.Register("ComboSelectionProperty", typeof(string), typeof(MainPage), new PropertyMetadata(null));
public string ComboSelection
{
get { return (string)GetValue(ComboSelectionProperty); }
set
{
SetValue(ComboSelectionProperty, value);
this.RaisePropertyChanged("ComboSelection");
}
}
private static readonly DependencyProperty ComboSourceProperty =
DependencyProperty.Register("ComboSourceProperty", typeof(List<Binding>), typeof(MainPage), new PropertyMetadata(null));
public List<Binding> ComboSource
{
get
{
return (List<Binding>)GetValue(ComboSourceProperty);
}
set
{
SetValue(ComboSourceProperty, value);
this.RaisePropertyChanged("ComboSource");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var temp = new List<Binding>();
temp.Add(new Binding() { code = "3", display = "Three" });
temp.Add(new Binding() { code = "4", display = "Four" });
this.ComboSource = temp;
this.ComboSelection = "3";
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
public class Binding
{
public string code {get; set;}
public string display { get; set; }
}
Not strictly MVVM, but to explain the problem, when the button click event is fired, the Combosource is changed with a new selection being made, however that selection does not bind and the problem i mentioned above starts happening.
Your SelectedValuePath is "display", which I assume is a string property of the Room class. But you're binding SelectedValue to the Room property of your viewmodel, and I assume this property is of type Room... So the SelectedValue is of the type string, and you're binding it to a property of type Room: it can't work because there is no conversion between those types.
Instead of using the SelectedValue property, why not use the SelectedItem ?
<ComboBox Name="RoomDropDown" Visibility="{Binding Path=RoomDropDownVisible,Mode=OneWay,Converter={StaticResource BoolVisibilityConvertor}}"
ItemsSource="{Binding Path=RoomList,Mode=OneWay}" DisplayMemberPath="display" SelectedItem="{Binding Path=Room,Mode=TwoWay}"/>
There seems to be a bug in ComboBox data binding where the binding will completely break if the data it is binding to SelectedValue becomes null.
Place a breakpoint in the ComboSelection setter and see if it is ever getting set to null. If this is the source of the problem, add this to your setter:
public string ComboSelection
{
// .....
set
{
if(value == null)
return;
// .....
}
}
On a side note, you probably don't need use a dependency property to back ComboSelection. The data binding for this should work just fine on a normal property as long as you keep using PropertyChanged.