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.
Related
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 View A that has a UserControl U containing its own UserControl UChild.
When I set the DataContext of A, will UChild get the DataContext of A using direct binding to its grandparent's property i.e. { Binding PropertyFromA } within UChild?
I am trying to achieve this but looks like grandchild in my case does not get the DataContext.
Edit:
Some code I use:
My binding happens inside the VM, where BookList is an ObservableCollection that gets filled with Books:
/// <summary>
///
/// </summary>
public bool FillBooksCtrlList()
{
List<Book> list = DBHelper.GetRecipes();
if (list == null)
{
return false;
}
else
{
BookList = new ObservableCollection<Book>(list);
foreach (Book book in BookList)
{
BookCtrlList.Add(new BookCtrl { DataContext = book, Margin = new Thickness(0, 10, 10, 0) });
}
return true;
}
}
In short, I am creating an instance of BookCtrl inside BookPanelCtrl
that belongs to the MainView.
Maybe the way I bind is the problem?
I think you want something like this. In your ViewModel you have an ObservableCollection containing Books:
public class SomeViewModel {
private ObservableCOllection<Book> _books;
public ObservableCollection<Book> Books
{
get
{
return _books;
}
set
{
_books = value;
RaisePropertyChanged("Books");
}
}
}
public bool FillBooksCtrlList()
{
List<Book> list = DBHelper.GetRecipes();
if (list == null)
{
return false;
}
else
{
Books = new ObservableCollection<Book>(list);
return true;
}
}
Then, in your View you could have something like:
<UserControl DataContext="SomeViewModel">
<ListBox ItemsSource="{Binding Books}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Title}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>
This way the UserControl is bound to the ViewModel, the ListBox is bound to your list of Books and each item in the ListBox is bound to the Title property of a book (I'm guessing books have a title).
Hope this helps.
I'm developing a WPF application and I'm struggling a little bit to understand some of the details of DataContext as it applies to binding. My application uses a business object which is defined like this:
public class MyBusinessObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
// enumerations for some properties
public enum MyEnumValues
{
[Description("New York")]
NewYork,
[Description("Chicago")]
Chicago,
[Description("Los Angeles")]
LosAngeles
}
// an example property
private string _myPropertyName;
public string MyPropertyName
{
get { return _myPropertyName; }
set
{
if (_myPropertyName == value)
{
return;
}
_myPropertyName = value;
OnPropertyChanged(new PropertyChangedEventArgs("MyPropertyName"));
}
}
// another example property
private MyEnumValues _myEnumPropertyName;
public MyEnumValues MyEnumPropertyName
{
get { return _myEnumPropertyName; }
set
{
if (_myEnumPropertyName== value)
{
return;
}
_myEnumPropertyName= value;
OnPropertyChanged(new PropertyChangedEventArgs("MyEnumPropertyName"));
}
}
// example list property of type Widget
public List<Widget> MyWidgets { get; set; }
// constructor
public MyBusinessObject()
{
// initialize list of widgets
MyWidgets = new List<Widget>();
// add 10 widgets to the list
for (int i = 1; i <= 10; i++)
{
MyWidgets.Add(new Widget());
}
// set default settings
this.MyPropertyName = string.empty;
}
}
As you can see, I have some properties that are declared in this class one of which is a list of Widgets. The Widget class itself also implements INotifyPropertyChanged and exposes about 30 properties.
My UI has a combobox which is bound to my list of Widgets like this:
MyBusinessObject myBusinessObject = new MyBusinessObject();
public MainWindow()
{
InitializeComponent();
this.DataContext = myBusinessObject;
selectedWidgetComboBox.ItemsSource = myBusinessObject.MyWidgets;
selectedWidgetComboBox.DisplayMemberPath = "WidgetName";
selectedWidgetComboBox.SelectedValuePath = "WidgetName";
}
The majority of the controls on my UI are used to display the properties of a Widget. When my user selects a Widget from the combobox, I want these controls to display the properties for the selected Widget. I'm currently achieving this behavior by updating my window's DataContext in the SelectionChanged event handler of my combobox like this:
private void selectedWidgetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.DataContext = selectedWidgetComboBox.SelectedItem;
}
This allows me to bind my controls to the appropriate Widget property like this:
<TextBox Text="{Binding WidgetColor}"></TextBox>
However, not all of the controls in my UI are used to display Widget properties. Some of the controls need to display the properties from MyBusinessObject (for example: MyPropertyName defined above). In this scenario, I can't simply say:
<TextBox Text="{Binding MyPropertyName}"></TextBox>
...because the DataContext of the window is pointing to the selected Widget instead of MyBusinessObject. Can anyone tell me how I set the DataContext for a specific control (in XAML) to reference the fact that MyPropertyName is a property of MyBusinessObject? Thank you!
Instead of changing the DataContext of your window, you should add a property to your MyBusinessObject class like this one:
private Widget _selectedWidget;
public Widget SelectedWidget
{
get { return _selectedWidget; }
set
{
if (_selectedWidget == value)
{
return;
}
_selectedWidget = value;
OnPropertyChanged(new PropertyChangedEventArgs("SelectedWidget"));
}
}
Then bind SelectedWidget to the SelectedItem property of your combobox. Anywhere that you need to use the widget's properties you can do this:
<TextBox Text="{Binding Path=SelectedWidget.WidgetColor}"></TextBox>
try
<TextBox Text="{Binding MyBusinessObject.MyPropertyName}"></TextBox>
this works if MyBusinessObject is the datacontext of the textbox and MyPropertyName is a property of MyBusinessObject
Also, Here is a good article to clarify binding
hope this helps
EDIT 1:
use a relative binding like this:
text="{Binding DataContext.MyPropertyName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TypeOfControl}}}"
So the relatve binding allows you to look up the visual tree to another UI element and use its datacontext. I would consider wrapping your window's contents in a grid. and wet your windows datacontext to the businessobject and the grids datacontext to the widget. That way you can always use the parent window's datacontext through the realtive source binding.
so use the following if your window's datacontext is your business object
text="{Binding DataContext.MyPropertyName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
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 screen with a ListBox of items. The item template contains an expander control with some of the data in the header and some of the data in the content part of the expander.
The data template for the ListBox ItemTemplate is similar to this:
<DataTemplate x:Key="MyTypeTemplate" DataType="{x:Type MyType}">
<Expander DataContext="{Binding}">
<Expander.Header>
<Canvas>
<TextBox Text="{Binding MyProperty}"/>
</Canvas>
</Expander.Header>
<Canvas>
<TextBox Text={Binding MyDetailedProperty}"/>
</Canvas>
</Expander>
</DataTemplate>
Whenever these properties change, either 'MyProperty' or 'MyDetailedProperty' changes, the expander control collapsed. I believe that is has something to do with the Expander item getting recreated when the data changes.
As an additional data item, the list being bound to the listbox implements IBindingList as it comes from a library created for .NET 2.0. I cannot recreate the list using ObservableCollection due to time constraints
I ended up wrapping my model objects in a view object that adds an IsExpandable property that I could bind to the Expanded IsExpanded property and then exposed the data.
This is not a general purpose solution but it solves my immediate problem. The possible issues that I see that I haven't explored are whether the PropertyChanged and ListChanged event attaches cause memory leak issues with my UI objects, but in my situation each object should only be created once.
Also, events beyond Add and Remove in the collection change are not supported, but in my case I'm not firing anything else so it is safe for me to ignore them.
public class ExpandableItem<T> : INotifyPropertyChanged
where T: INotifyPropertyChanged
{
private bool m_isExpanded;
private readonly T m_data;
public ExpandableItem(T data)
{
m_data = data;
m_data.PropertyChanged +=
delegate
{
PropertyChanged(this, new PropertyChangedEventArgs("Data"));
};
}
public bool IsExpanded
{
get { return m_isExpanded; }
set
{
if (value != m_isExpanded)
{
m_isExpanded = value;
PropertyChanged(this, new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public T Data
{
get
{
return m_data;
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
public class ExpandableList<TObject,TList> :
ObservableCollection<ExpandableItem<TObject>>
where TList : ObservableCollection<TObject>
where TObject : INotifyPropertyChanged
{
readonly TList m_list;
public ExpandableList(TList list)
: base(list.Select(obj=>new ExpandableItem<TObject>(obj)))
{
list.CollectionChanged += OnListChanged;
m_list = list;
}
public TList Data { get { return m_list; } }
private void OnListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
Insert(e.NewStartingIndex, e.NewItems[0]);
}
if (e.Action == NotifyCollectionChangedAction.Remove)
{
RemoveAt(e.OldStartingIndex);
}
}
}