I have a custom control to show items with checkboxes inside a ComboBox. To realize this, I used a DataTemplate with a CheckBox. The ItemSource of the ComboBox uses a binding to a ObserableCollection<FilterValue> which contains my filter values. FilterValue is a custom class implementing INotifyPropertyChanged. The properties Content and IsChecked of the CheckBox use bindings as well to use the values of my list. This control will be used in Silverlight.
Binding itself works fine, as seen here:
The problem appears when I register the Checked or Unchecked event.
As soon as one of the check boxes changed its state, the event is fired as expected but at this moment, the value in the bound list is still not updated.
What I saw while debugging is that the Checked/Unchecked events are firing before the PropertyChanged event of the FilterValue.
This means that at the time the event is firing, I can't ask the list for all active (checked) filters. What could I do to achieve this?
FilterControl.xaml:
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:sdk="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
xmlns:local="clr-namespace:Controls" x:Class="Controls.FilterControl"
mc:Ignorable="d"
d:DesignHeight="45" d:DesignWidth="140">
<StackPanel x:Name="LayoutRoot">
<sdk:Label x:Name="LblFilterDescription" Content="-" />
<ComboBox x:Name="Filter" Width="120" ItemsSource="{Binding AvailableFilters, RelativeSource={RelativeSource FindAncestor, AncestorType=local:FilterControl}}">
<ComboBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding Path=Text}" IsChecked="{Binding Path=IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Checked="FilterChanged" Unchecked="FilterChanged" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</UserControl>
FilterControl.xaml.cs:
public partial class FilterControl : UserControl
{
public delegate void FilterChangedHandler(object sender);
public event FilterChangedHandler OnFilterChanged;
public ObservableCollection<FilterValue> AvailableFilters { get; set; }
public List<string> AppliedFilters
{
get
{
return new List<string>(AvailableFilters.Where(filter => filter.IsChecked).Select(filter => filter.Text));
}
}
public FilterControl()
{
InitializeComponent();
AvailableFilters = new ObservableCollection<FilterValue>();
}
public bool AddFilterValue(string filterValue)
{
bool found = false;
foreach (FilterValue f in AvailableFilters)
{
if (f.Text == filterValue)
{
found = true;
break;
}
}
if (!found)
AvailableFilters.Add(new FilterValue() { IsChecked = false, Text = filterValue });
return found;
}
private void FilterChanged(object sender, RoutedEventArgs e)
{
//Here if I check AvailableFilters, the value is not changed yet.
//PropertyChanged allways fires after this, what makes me unable to
//get all currently applied filters (checked items)...
}
}
FilterValue:
public class FilterValue : INotifyPropertyChanged
{
private bool _IsChecked;
private string _Text;
public bool IsChecked
{
get { return _IsChecked; }
set
{
_IsChecked = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IsChecked"));
}
}
public string Text
{
get { return _Text; }
set
{
_Text = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Text"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So, as I tried to reproduce this behavior, I realized that this appears to be a behavior that only occurs like that in Silverlight. If you try this example on WPF, the Changed fires after the bound property is updated. So you can just access your AppliedFilters property in the FilterChanged method and it will reflect the actual current situation. On Silverlight though, not so much. Even worse, this behavior didn’t even appear to be consistent to me. I did encounter situations in which the event fired after the property has been updated (resulting in the expected output).
A way to get around this is to clean up your component logic. If you look at it, you are mixing two different concepts: Event-driven UI logic, and clear data binding. Of course, doing it “properly” has multiple effects you likely cannot just ensure in an existing project, but you can at least try to get in the right direction here which should then also solve this issue.
So your logic right now uses data binding to provide the data for the view, and to reflect changes of the displayed items. But you are using events on the item level to perform additional logic depending on the former changes. As we have seen, the order of execution appears not be guaranteed across platforms, so it’s best to avoid having to rely on it.
In this case, you should have your data be the source of truth and make changes in the data tell you when applied filters change. You’re already halfway there by having an ObservableCollection and items that implement INotifyPropertyChanged. Unfortunately, an observable collection will only notify you about changes to the collection but not to changes to the contained items. But there are multiple solutions to expand the collection to also look at the items inside the collection.
This related question covers exactly that topic and there are multiple ideas on how to expand the observable collection for exactly that behavior. In my case, I have used the FullyObservableCollection implementation by Bob Sammers.
All you have to do for that is to change your ObservableCollection<FilterValue> into a FullyObservableCollection<FilterValue> and subscribe to the ItemPropertyChanged event:
AvailableFilters = new FullyObservableCollection<FilterValue>();
AvailableFilters.ItemPropertyChanged += AvailableFilters_ItemPropertyChanged;
In that event handler, you will then correctly see the proper behavior.
Related
In my WPF app I have a TreeView with a lot of elements, so I turned on virtualization to speed up the rendering, like this:
<TreeView VirtualizingPanel.IsVirtualizing="True">
...
</TreeView>
However, if I do this it seems controls inside the tree items that are data-bound to properties in my Viewm Model stop reacting to the OnPropertyChanged event.
For example, let's assume I have the following template for my items:
<DataTemplate DataType="{x:Type viewModels:MyViewModel}">
<TextBlock Text="{Binding ItemName}" />
</DataTemplate>
and the model is something like this:
public class MyViewModel: INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
private string itemName;
public string ItemName {
get { return itemName; }
set {
if(value != itemName) {
itemName = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ItemName)));
}
}
}
}
The initial binding to the property works correctly and the item name is displayed in the TextBlock, but if the value of the ItemName property changes after the tree view item has already been rendered it will not update in the UI, as if the PropertyChanged event is being completely ignored.
How can I fix this? Note that if I set VirtualizingPanel.IsVirtualizing="False" the problem disappears, so it is definitely caused by the virtualization.
I've found the problem after further testing and debugging, it is not strictly related to the panel virtualization, I guess that was just a condition in which the bug manifests itself.
Basically, in my full code (that I omitted for brevity) the ItemName property is being set from inside an Event Handler. The problem was that my handler was triggered by events that were raised in a background thread somewhere else in the code and thus it did not run on the UI thread. I was under the impression that this wasn't a problem, since I was not manipulating any UI objects in the model (I was just touching my ViewModel object), however apparently if you're not in the UI thread raising the PropertyChanged becomes unreliable.
To solve the problem, I've wrapped the code in my handler in a dispatcher call, like this:
private void MyEvenHandler(object sender, MyEventArgs e)
{
Dispatcher.Invoke(() =>
{
var myViewModel = FindVm(e);
myViewModel.ItemName = "updated";
}
}
Now everything works as expected.
I have a panel with tabs. My view model for this panel contains ObservableCollection of view models for tabs, and a property for selected tab.
When some action requests to focus a tab, or a new tab is created, I change Selected and tab selection changes properly, well almost, because the content is valid, but all headers look like nothing is selected.
I found a solution that says to add IsAsync=True to my binding. This solved the problem but added a bunch of new issues.
First thing is that when I run program in debug mode, adding tabs with buttons works ok, tabs get switched and selected properly but when I try to click a tab to select it I get exception
The calling thread cannot access this object because a different thread owns it.
it is thrown while setting property representing currently selected tab:
private Tab selected;
public Tab Selected
{
get { return Selected; }
set { SetProperty(ref Selected, value); } // <<< here (I use prism BindableBase)
}
Other problem is that when I quickly switch tabs, it can come to a situation where I have Tab1 selected but it shows content of Tab2, switching tabs couple more times gets things back to work.
My question is, how can I solve this, i.e. have my tab headers selected (kind of highlighted) when Selected is changed, without having issues that assing IsAsync causes.
Edit
Here is the code that allows to reproduce issues. It uses prism 6.1.0
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<DockPanel>
<StackPanel DockPanel.Dock="Top"
Orientation="Horizontal"
Margin="0,5"
Height="25">
<Button
Command="{Binding AddNewTabCommand}"
Content="New Tab"
Padding="10,0"/>
<Button
Command="{Binding OtherCommand}"
Content="Do nothing"
Padding="10,0"/>
</StackPanel>
<TabControl
SelectedItem="{Binding Selected, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, IsAsync=True}" <!--remove IsAsync to break tab header selecting-->
ItemsSource="{Binding Tabs}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Margin="5"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<TextBox Text="{Binding Text}"/>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new TabGroup();
}
}
Tab.cs
public class Tab : BindableBase
{
public Tab(string name, string text)
{
this.name = name;
this.text = text;
}
private string name;
public string Name
{
get { return name; }
set { SetProperty(ref name, value); }
}
private string text;
public string Text
{
get { return text; }
set { SetProperty(ref text, value); }
}
}
TabGroup.cs
public class TabGroup : BindableBase
{
private Random random;
public TabGroup()
{
this.random = new Random();
this.addNewTabCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(AddNewTab, () => true));
this.otherCommand = new Lazy<DelegateCommand>(() => new DelegateCommand(Method, () => Selected != null).ObservesProperty(() => Selected));
Tabs.CollectionChanged += TabsChanged;
}
private void Method()
{
}
private void TabsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var newItems = e.NewItems?.Cast<Tab>().ToList();
if (newItems?.Any() == true)
{
Selected = newItems.Last();
}
}
private void AddNewTab()
{
Tabs.Add(new Tab(GetNextName(), GetRandomContent()));
}
private string GetRandomContent()
{
return random.Next().ToString();
}
private int num = 0;
private string GetNextName() => $"{num++}";
private Tab selected;
public Tab Selected
{
get { return selected; }
set { SetProperty(ref selected, value); }
}
public ObservableCollection<Tab> Tabs { get; } = new ObservableCollection<Tab>();
private readonly Lazy<DelegateCommand> addNewTabCommand;
public DelegateCommand AddNewTabCommand => addNewTabCommand.Value;
private readonly Lazy<DelegateCommand> otherCommand;
public DelegateCommand OtherCommand => otherCommand.Value;
}
Preparing this let me figure where does the exception come from. It is because the OtherCommand observes selected property. I still don't know how to make it right. Most important for me is to get tabs to be selected when they should be and so that selected tab won't desynchronize with what tab control shows.
Here is a github repo with this code
https://github.com/lukaszwawrzyk/TabIssue
I'll focus on your original problem, without the async part.
The reason why the tabs are not properly selected when adding a new tab is because you set the Selected value in the CollectionChanged event handler. Raising an event causes sequential invocation of handlers in order in which they were added. Since you add your handler in the constructor, it will always be the first one to be invoked, and what's important, it will always be invoked before the one that updates the TabControl. So when you set the Selected property in your handler, TabControl doesn't yet "know" that there's such a tab in the collection. More precisely, the header container for the tab is not yet generated, and it cannot be marked as selected (which causes the visual effect you're missing), moreover, it won't be when it's finally generated. TabControl.SelectedItem is still updated, so you see the content of the tab, but it also causes header container previously marked as selected to be unmarked, and you eventually end up with no tab visibly selected.
Depending on your needs, there are several ways to solve this problem. If the only way of adding new tabs is through the AddNewTabCommand, you could just modify the AddNewTab method:
private void AddNewTab()
{
var tab = new Tab(GetNextName(), GetRandomContent());
Tabs.Add(tab);
Selected = tab;
}
In this case you should not set the Selected value in the CollectionChanged handler, because it will prevent PropertyChanged from being raised at the right time.
If AddNewTabCommand is not the only way of adding tabs, what I usually do is to create a dedicated collection which would do the required logic (this class is nested in TabGroup):
private class TabsCollection : ObservableCollection<Tab>
{
public TabsCollection(TabGroup owner)
{
this.owner = owner;
}
private TabGroup owner;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e); //this will update the TabControl
var newItems = e.NewItems?.Cast<Tab>()?.ToList();
if (newItems?.Any() == true)
owner.Selected = newItems.Last();
}
}
Then simply instantiate the collection in the TabGroup constructor:
Tabs = new TabsCollection(this);
If this scenario appears in various places and you don't like repeating your code, you could create a reusable collection class:
public class MyObservableCollection<T> : ObservableCollection<T>
{
public event NotifyCollectionChangedEventHandler AfterCollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
base.OnCollectionChanged(e);
AfterCollectionChanged?.Invoke(this, e);
}
}
and then subscribe to AfterCollectionChanged whenever you need to be sure that all CollectionChanged subscribers have been notified.
When you get the error "The calling thread cannot access this object because a different thread owns it." this means that you are trying to access an object on another concurrent thread. To show you how to resolve this i want to give an example. First you have to find every runtime objects, like listboxes and listviews and such. (Basically GUI controls). They run on a GUI thread. When you try to run them on another thread forexample a backgroundworker or an task thread, the error appears. So this is what you want to do:
//Lets say i got a listBox i want to update in realtime
//this method is for the purpose of the example running async(background)
public void method(){
//get data to add to listBox1;
//listBox1.Items.Add(item); <-- gives the error
//what you want to do:
Invoke(new MethodInvoker(delegate { listBox1.Items.Add(item); }));
//This invokes another thread, that we can use to access the listBox1 on.
//And this should work
}
Hope it helps.
I am binding a Collection at run time to a Combobox and I would like to set the Index after to 0. I could not find a straight answer to what I want.
_stationNames = new ObservableCollection<string>(_floorUnits.Unit.Select(f => f.Name));
_stationNames.Insert(0, "All");
stationsComboBox.ItemsSource = _stationNames;
stationsComboBox.SelectedIndex = 0;//Doesn;t work
Xaml
<ComboBox x:Name="stationsComboBox" Grid.Row="1" Grid.Column="1" Text="{Binding Name}"
SelectionChanged="StationComboBoxSelectionChanged" VerticalAlignment="Center" Margin="3"
SelectedIndex="0"/>
It sounds like you're trying to use it like you would with WinForms. WPF is a slightly different beast and a lot more powerful regarding bindings.
I recommend reading a bit on MVVM to get the most benefit from WPF. By binding the XAML to a view model class (rather than trying to wire things up in Code-behind) you will find you can accomplish what you want with a lot more flexibility without oodles of code.
For instance: Given the following VM:
public class MyViewModel: INotifyPropertyChanged
{
public ObservableCollection<string> StationNames
{
get;
private set;
}
public Something()
{
StationNames = new ObservableCollection<string>( new [] {_floorUnits.Unit.Select(f=>f.Name)});
StationNames.Insert(0, "All");
}
private string _selectedStationName = null;
public string SelectedStationName
{
get
{
return _selectedStationName;
}
set
{
_selectedStationName = value;
FirePropertyChanged("SelectedStationName");
}
}
private void FirePropertyChanged(string propertyName)
{
if ( PropertyChanged != null )
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
You can set your view's (XAML form) DataContext to an instance of the ViewModel and update your combo box definition to:
<ComboBox x:Name="stationsComboBox" Grid.Row="1" Grid.Column="1"
ItemsSource="{Binding Path=StationNames}" SelectedItem={Binding Path=SelectedStationName} VerticalAlignment="Center" Margin="3"
SelectedIndex="0"/>
From here whenever the combo box selection changes, the VM's SelectedStationName updates to reflect the current selection, and from anywhere in the VM code, setting the VM's SelectedStationName will update the combo's selection. (I.e. implementing a Reset button, etc.)
Normally though, with something like what you've suggested, I would be looking at binding directly to the Units collection. (or VM's derived from units if they themselves can be viewed/edited.) In any case it should give you a bit of a starting point to start researching into WPF bindings.
I'm trying to solve a simple problem using LINQ, which I'm just learning.
I have a collection of strings, in this case representing serial ports, that will be displayed in a control, but must be ordered. The original collection is unsorted, and I don't necessarily want to modify it, or make a copy of it. So, I created a property of type IEnumerable and bound it to a ComboBox.
This works great, the ComboBox has the correct contents in the correct order. However, if the original collection changes, either a) The ComboBox doesn't get notified properly when the original collection changes, or b) the LINQ query isn't being refreshed.
After trying some different things, I can't see how the following code doesn't work. I must be missing something.
There might be extra code here that is redundant... Anyway, the source followed by the XAML for the controls:
public partial class MainWindow : Window
{
ObservableCollection<string> original = new ObservableCollection<string>();
public ObservableCollection<string> OriginalList {
get { return (original); }
}
private IEnumerable<string> _portList;
public IEnumerable<string> PortList {
get { return (_portList); }
}
public MainWindow() {
InitializeComponent();
original.Add("COM5");
original.Add("COM1");
original.Add("COM3");
original.Add("COM4");
original.Add("COM2");
original.CollectionChanged += new NotifyCollectionChangedEventHandler(OriginalChanged);
_portList = (
from port in original
orderby port ascending
select port
);
DataContext = this;
}
private void AddPortButton_Click(object sender, RoutedEventArgs e) {
original.Add("COM2.5");
}
void OriginalChanged(Object sender, NotifyCollectionChangedEventArgs args) {
NotifyPropertyChanged("PortList");
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
The XAML:
<ComboBox Name="SerialPortListBox" ItemsSource="{Binding PortList}" Width="100" />
<ComboBox Grid.Row="1" Name="OriginalListBox" ItemsSource="{Binding OriginalList}" Width="100" Margin="0,5,0,0"/>
<Button Grid.Column="1" Name="AddPortButton" Content="Add Port 2.5" Width="100" Margin="10,0,0,0" Click="AddPortButton_Click" />
I think you will only get the change notifications you want by binding your ComboBox to an ObservableCollection. This class implements the interface INotifyCollectionChanged, which is what the innards of WPF rely on to notify the UI that updates are required.
I see you are trying to get around this by implementing INotifyPropertyChanged for PortList, however this won't work the way you want. That interface does not trigger the appropriate event to trigger the combobox to refresh. INotifyCollectionChanged tells the listener that a collection has changes (i.e; 'Add', 'Remove', 'Move', 'Replace', 'Reset'), whereas INotifyPropertyChanged only indicates that some value has changed in the bound object. The ComboBox will not respond to an INotifyPropertyChanged event, in fact it's probably not even subscribing to events of that type.
So, either bind directly to the underlying datasource, or implement a 2nd ObservableCollection on top of that, rather than only an IEnumerable, which does not notify on change.
I have made a tree View in wpf Using MVVM .
it is working fine but here is one problem that leaf node contains some checkboxes and user have only two options either to select one or none .
So here how i can restricted user to select maximum only one cold drink.
I did one trick but it didn't work that when i have already selected a drink and then i select another one than i set the last selected value in the observable collection to false but it doesn't affect on view and selected check boxes remains selected although in collection only one option's value is true.
I cant use radio button instedof checkbox becasue user can select none of the options and i cant give an additional option for none of the above.
If any one have any solution so please let me know I'll be very thankful.
updated question:
i think i didn't define my problem in a proper way so i am giving my code snipperts here hope by this i'll get the solution o f my problem...
My View Model Class
namespace TestViewModels
{
public class ViewModel :ViewModelBase
{
private ObservableCollection<AvailableProducts> _MyTreeViewProperty
public ObservableCollection<AvailableProducts> MyTreeViewProperty
{
get { return _MyTreeViewProperty
set { _MyTreeViewProperty value;
RaisePropertyChanged("MyTreeViewProperty");}
}
}
public class AvailableProducts
{
private string _BrandName;
public string BrandName
{
get { return _BrandName
set { _BrandName = value; }
}
private bool _IsExpanded;
public bool IsExpanded
{
get
{
return _IsExpanded;
}
set
{
_IsExpanded = value;
}
}
private ObservableCollection<ProductTypes> _MyProductTypes
public ObservableCollection<ProductTypes> MyProductTypes
{
get { return _MyProductTypes}
set { _MyProductTypes= value; }
}
}
public class ProductTypes
{
private string _ProductTypeName;
public string ProductTypeName
{
get { return _ProductTypeName;
set { _ProductTypeNamevalue; }
}
private ObservableCollection<ProductSubTypes> _ProdSubTypes;
public ObservableCollection<ProductSubTypes> ProdSubTypes
{
get { return _ProdSubTypes;}
set { _ProdSubTypes;= value; }
}
}
public class ProductSubTypes
{
private string _ProductSubTypeName;
public string ProductSubTypeName
{
get { return _ProductSubTypeName;
set { _ProductSubTypeName;}
}
private int _ParentID;
public int ParentID
{
get { return _ParentID;}
set { _ParentID;= value; }
}
private bool _IsAssigned;
public bool IsAssigned
{
get { return _IsAssigned; }
set
{
_IsAssigned = value;
if _ParentID;!= 0)
{
//updating data in database
//Calling and setting new collection value in property
//issue : updated collection sets in setter of MyTreeViewProperty but before calling getter
// it comes to IsAssigned getter so view doesnt get updated collection of MyTreeViewProperty
}
RaisePropertyChanged("IsAssigned");
}
}
}
}
View
<Page x:Class="ShiftManagerViews.Pages.ProductTreeSelection
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding ProductsTree, Source={StaticResource Locator}}"
mc:Ignorable="d" Width="870" Height="665"
>
<TreeView Margin="10,10,0,13" ItemsSource="{Binding MyTreeViewProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left"
VerticalAlignment="Top" Width="800" Height="Auto" MinHeight="400" MaxHeight="800">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:AvailableProducts}"
ItemsSource="{Binding MyProductTypes}">
<WrapPanel>
<Image Width="20" Height="20" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding BrandName}" FontSize="14"/>
</WrapPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:ProductTypes}"
ItemsSource="{Binding ProdSubTypes}">
<WrapPanel>
<Image Width="18" Height="15" Source="/ShiftManagerViews;component/Images/12.bmp"/>
<Label Content="{Binding ProductTypeName}" FontSize="13"/>
</WrapPanel>
</HierarchicalDataTemplate>
<!-- the template for showing the Leaf node's properties-->
<DataTemplate DataType="{x:Type local:ProductSubTypes}">
<StackPanel>
<CheckBox IsChecked="{Binding IsAssigned, Mode=TwoWay}" Content="{Binding ProductSubTypeName}" Height="25">
</CheckBox>
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
What about using a ListBox to display sub-items instead of a TreeView? You can style that so the items contain a CheckBox to show IsSelected instead of highlighting the item.
I'd suggest your user interface is wrong. If the user can only pick one then it would be better to swap these for radio buttons and add a "None of the above" option. That'll then give you the behaviour you want for free and your UI will be more intuitive.
EDIT: Since you say you can't add a "None" option and want to use a checkbox (even though I strongly disagree on checkboxes where a radio button is more appropriate - a common UI error)...
The technical problem you are probably facing is that an ObservableCollection only raises notification events if the collection itself changes. i.e. Only if items are added or removed. It does not raised events when items within the collection change, therefore the changing the status of the checkbox in the code will not raise the event for the UI binding to act on.
One solution to this to write a custom class that extends ObservableCollection that does provide this behaviour
From MSDN:
If you need to know if someone has changed a property of one of the
items within the collection, you'll need to ensure that the items in
the collection implement the INotifyPropertyChanged interface, and
you'll need to manually attach property changed event handlers for
those objects. No matter how you change properties of objects within
the collection, the collection's PropertyChanged event will not fire.
As a matter of fact, the ObservableCollection's PropertyChanged event
handler is protected—you can't even react to it unless you inherit
from the class and expose it yourself. You could, of course, handle
the PropertyChanged event for each item within the collection from
your inherited collection
I upvoted Rachel's answer, it is a common way in WPF to databind sets of radio buttons or check boxes. If you still want to go the tree view way, below code works. All view related code is in the view, so below code follows MVVM principles. If you are a MVVM purist you can put the code behind and a TreeView control in a user control if you do not want any code behind.
XAML:
<TreeView ItemsSource="{Binding Path=Drinks}">
<TreeView.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding .}" Checked="OnCheckBoxChecked" Unchecked="OnCheckBoxUnchecked" Loaded="OnCheckBoxLoaded" />
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
Code behind + VM:
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new VM();
}
private void OnCheckBoxChecked(object sender, System.Windows.RoutedEventArgs e)
{
foreach (CheckBox checkBox in _checkBoxes.Where(cb => cb != sender))
{
checkBox.IsChecked = false;
}
(DataContext as VM).CurrentDrink = (sender as CheckBox).Content.ToString();
}
private void OnCheckBoxUnchecked(object sender, System.Windows.RoutedEventArgs e)
{
(DataContext as VM).CurrentDrink = null;
}
private void OnCheckBoxLoaded(object sender, System.Windows.RoutedEventArgs e)
{
_checkBoxes.Add(sender as CheckBox);
}
private List<CheckBox> _checkBoxes = new List<CheckBox>();
}
public class VM
{
public List<string> Drinks
{
get
{
return new List<string>() { "Coffee", "Tea", "Juice" };
}
}
public string CurrentDrink { get; set; }
}
I did one trick but it didn't work that when i have already selected a
drink and then i select another one than i set the last selected value
in the observable collection to false but it doesn't affect on view
and selected check boxes remains selected although in collection only
one option's value is true.
Make sure that your child objects (AvailableProducts
and SubProductTypes) also implement INotifyPropertyChanged, this will make sure that the UI receives changes when modify the object.
Once all of you objects update the UI properly you will be able to layer in, and test, whatever custom business logic you need.
So if you have a product type that can only have one sub chosen, you could add a property on ProductType called OnlyAllowOneChild. Whenever, a child object raises a IsAssigned changed event, the parent can set false all other children. This of course requires you to have the parent either register for the children's PropertyChangedEvent, or got grab an EventAggregator (MVVMLight Messenger, or PRISM EvenAggregator) and create a messaging system.
Finally i am succeeded to solve my problem.
on Is Assigned property i am updating my database values and calling a method in view using MVVM Light messaging and passing currently selected leaf's parent id in it as a parameter...
Added a property in class Product Types to expand the parent node of the last selected leaf..
In view's method i am refreshing data context's source and passing currently selected leaf's parent id tO the VM to set its Is Expanded property value to true...
By this my view is working perfectly as same as i want...
If any body have solution better than this than I'll be happy to know.