WPF ListBox Template bindings update only on scrolling - c#

I have the following ListBox with the ContentControl as DataTemplate:
<ListBox x:Name="lstActionConfigs" ItemsSource="{Binding Path=AllActionConfigList}" SelectedItem="{Binding Path=ListSelectedItem, Mode=TwoWay}" HorizontalContentAlignment="Stretch" Grid.Row="3" Margin="0,0,0,5">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type helper:ItemDetails}">
<ContentControl Template="{StaticResource ResourceKey=actionDetailsListItemTemplate}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<i:Interaction.Behaviors>
<behaviours:BringIntoViewBehaviour CustomIsSelected="{Binding Path=IsSelected, Mode=TwoWay}"/>
</i:Interaction.Behaviors>
</ContentControl>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Each bounded instance has 'IsSelected' property which notify the UI on changes via INotifyPropertyChanged:
public bool IsSelected
{
get { return isSelected; }
set
{
isSelected = value;
notify("IsSelected");
}
}
I built a custom behavior that brings into view the elements that changed it's IsSelectedProperty to true, as the follows:
public class BringIntoViewBehaviour : Behavior<FrameworkElement>
{
public bool CustomIsSelected
{
get { return (bool)GetValue(CustomIsSelectedProperty); }
set { SetValue(CustomIsSelectedProperty, value); }
}
public static readonly DependencyProperty CustomIsSelectedProperty =
DependencyProperty.Register("CustomIsSelected", typeof(bool), typeof(BringIntoViewBehaviour), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(customIsSelectedPropertyChanged_Callback)));
private static void customIsSelectedPropertyChanged_Callback(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
BringIntoViewBehaviour thisControl = o as BringIntoViewBehaviour;
if (thisControl == null)
return;
bringIntoView(thisControl);
}
}
This item is not presented this moment on the UI as it located at the bottom of the list (there is a scroll bar).
I updated the IsSelected property with true value.
However, the customIsSelectedPropertyChanged_Callback method should be executed as we updated it's bounded property.
But, in practice, this method is invoked only when this item is presented on UI when moving the scroll bar down to it.

The reason most likely is UI virtualization. ListBox items host is by default VirtualizingStackPanel. It will not generate items which are out of view now, so when you set IsSelected on your model, your DataTemplate together with your behaviour are not created yet. Only when you scroll down, control is created together with behaviour from data template, and after it is bound CustomIsSelectedProperty is set to true, so your callback is called.
To verify this assumption you can disable UI virtualization for your ListBox and see if that resolves the problem.

Related

MVVM - get selected items from ListBox via Data Binding

I am writing app using MVVM pattern in C#.
My goal is to get selected items from ListBox in my own User Control.
I have created bindable object, with method to change this object (called when something new is selected):
public partial class MyUserControl : UserControl
{
...
public IEnumerable SelectedItems
{
get { return (IEnumerable)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
public static readonly DependencyProperty SelectedItemsProperty =
DependencyProperty.Register("SelectedItems", typeof(IEnumerable),
typeof(MyUserControl),
new FrameworkPropertyMetadata(default(IEnumerable),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public MyUserControl ()
{
InitializeComponent();
}
private void SelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectedItems = ListBox.SelectedItems;
}
}
There is also Items part, and in xaml part ListBox is named ListBox:
<ListBox Name="ListBox" SelectionChanged="SelectionChanged" ... />
This how it looks in page with ViewModel, in which MyUserControl is created:
<uc:MyUserControl ... SelectedItems="{Binding Path=MyObjectItems}" />
And here comes the problem. When setting SelectedItems in ViewModel:
private ObservableCollection<MyObject> _myObjectItems;
public ObservableCollection<MyObject> MyObjectItems
{
get { return _myObjectItems; }
set { _myObjectItems = value; }
}
No matter what I do, value will always be null. This also means, that SelectedItems in MyUserControl is null, too.
I can, for example use OneWayToSource binding mode:
<uc:MyUserControl ... SelectedItems="{Binding Path=MyObjectItems, Mode=OneWayToSource}" />
value is still null, same as MyObjectItems, but at least SelectedItems in MyUserControl contains selected items. Not good enough :/
After hours of trying different approaches I've found NuGet package Extended WPF Toolkit. List SelectedItemsOverride from class CheckListBox allows to bind list of selected items:
<UserControl x:Class="View.UserControls.MyUserControl"
...
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
x:Name="Root">
<Grid>
<xctk:CheckListBox Name="ListBox"
ItemSelectionChanged="SelectionChanged"
SelectedItemsOverride="{Binding SelectedItems, ElementName=Root}"
... />
</Grid>
</UserControl>
And because of that, binding:
<uc:MyUserControl ... SelectedItems="{Binding Path=MyObjectItems}" />
works! I have access to selected items in View Model - everything in simple way.

TextBox.LineCount and TextBox.GetLastVisibleLineIndex() is always -1 when using MVVM pattern on WPF

I have a TabControl in my View, and I dynamically add TabItems which contains a textbox as a content. And when want to get Line Count from Selected Item, it always returns -1, also with textbox.GetLastVisibleLineIndex(). Code is below:
My View:
<TabControl x:Name="tabControl" HorizontalAlignment="Left" Height="385" Margin="5,50,0,0" VerticalAlignment="Top" Width="740" ItemsSource="{Binding Tabs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedItem="{ Binding SelectedTab, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}">
<TabControl.ItemTemplate>
<!-- this is the header template-->
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<!-- this is the body of the TabItem template-->
<DataTemplate>
<TextBox
Text="{Binding Text, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,diag:PresentationTraceSources.TraceLevel=High }" AcceptsReturn="True" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="KeyUp">
<cmd:EventToCommand Command="{Binding DataContext.TextChanged, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
My ViewModel:
TabItem tabItem = new TabItem();
tabItem.Header = mainModel.Header;
TextBox textBox = new TextBox();
textBox.Text = mainModel.TextFile;
tabItem.LayoutUpdated += (sender2, e2) => textBox_LayoutUpdated(sender2, e2);
textBox.LayoutUpdated += (sender3, e3) => textBox_LayoutUpdated(sender3, e3);
tabItem.Content = textBox;
Tabs.Add(tabItem);
SelectedTab = tabItem;
private void textBox_LayoutUpdated(object sender, EventArgs args)
{
lineCount = ((SelectedTab as TabItem).Content as TextBox).LineCount;
}
MainModel is my Model in MVVM.
My View.cs:
this.UpdateLayout();
TabItem tab = this.tabControl.SelectedItem as TabItem;
int index = ((this.tabControl.SelectedItem as TabItem).Content as TextBox).GetLastVisibleLineIndex();
Even here in View.cs is always -1;
I am new in WPF MVVM,
Thanks.
You have many issues with your current code. It seems that you actually missed the point of MVVM.
First of all, a view-model must not be aware of the view, nor any view-specific types (e.g. TabItem). A view-model is just a layer that adopts your model in such way that this model can be presented by the view. A view-model must not construct the view itself, as you do it in your example.
The reason why you get -1 is that the TextBox you add into the tab item will never be laid out, because you override the tab item's ContentTemplate.
There are some other things you're either doing wrong or they're unnecessary:
Binding for the tab control's ItemsSource must not be TwoWay, because the tab control itself will never update this property value. The UpdateSourceTrigger is not needed here for the same reason.
UpdateSourceTrigger is not needed for the SelectedItem binding, because it has by default the PropertyChanged mode
you have a command named TextChanged, but by convention it has to be named TextChangedCommand (with a Command postfix)
tabItem.LayoutUpdated += (sender2, e2) => textBox_LayoutUpdated(sender2, e2) is an overkill for an event subscription that creates an unnecessary lambda capturing this, use the method group syntax instead: tabItem.LayoutUpdated += textBox_LayoutUpdated
And now the true MVVM solution:
Suppose you have the item's view-model:
class Item : ViewModelBase
{
public Item(string header, string textFile)
{
Header = header;
this.textFile = textFile;
}
public string Header { get; }
private string textFile;
public string TextFile
{
get => textFile;
set { textFile = value; OnPropertyChanged(); }
}
private int lineCount;
public int LineCount
{
get => lineCount;
set { lineCount = value; OnPropertyChanged(); Debug.WriteLine("Line count is now: " + value); }
}
}
This view-model represents a single item that will be displayed as a tab item. But maybe this will be some other control in the future - actually you don't have to bother with that. The view-model has no idea which controls exactly will display the values. The view-model just provides these values in a convenient way.
So, the Header and the TextFile properties contain the model values. The LineCount property will be calculated by the view (more on that see below).
The main view-model for that would look like this:
class ViewModel : ViewModelBase
{
public ObservableCollection<Item> Items { get; } = new ObservableCollection<Item>();
private Item selectedItem;
public Item SelectedItem
{
get => selectedItem;
set { selectedItem = value; OnPropertyChanged(); }
}
}
Note that the Items collection property is read-only. That means no one can change the reference to the collection, but the collection itself is not read-only. The SelectedItem reference may be updated however.
And now the important point: the LineCount property of the TextBox will also be updated, e.g. when the TextBox wraps the text and the control is resized. So we can't just calculate the lines count in view-model, we need to do this in the view.
However, the view-model, as we know, must not be aware of the view. What to do? In such cases a good developer prefers a Behavior from the System.Windows.Interactivity namespace.
Let's create a simple behavior that will monitor the LineCount of a TextBox:
class LineCountBehavior : Behavior<TextBox>
{
public int LineCount
{
get { return (int)GetValue(LineCountProperty); }
set { SetValue(LineCountProperty, value); }
}
public static readonly DependencyProperty LineCountProperty =
DependencyProperty.Register("LineCount", typeof(int), typeof(LineCountBehavior), new PropertyMetadata(0));
protected override void OnAttached()
{
AssociatedObject.LayoutUpdated += RefreshLineCount;
AssociatedObject.TextChanged += RefreshLineCount;
}
protected override void OnDetaching()
{
AssociatedObject.LayoutUpdated -= RefreshLineCount;
AssociatedObject.TextChanged -= RefreshLineCount;
}
private void RefreshLineCount(object sender, EventArgs e)
{
LineCount = AssociatedObject.LineCount;
}
}
Now we can attach this behavior to any TextBox and use the behavior's LineCount dependency property as a binding source. Here is a full XAML setup:
<TabControl ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<TabControl.ItemTemplate>
<DataTemplate DataType="local:Item">
<TextBlock Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate DataType="local:Item">
<TextBox Text="{Binding TextFile, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" AcceptsReturn="True" TextWrapping="Wrap">
<i:Interaction.Behaviors>
<local:LineCountBehavior LineCount="{Binding LineCount, Mode=OneWayToSource}"/>
</i:Interaction.Behaviors>
</TextBox>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
So this is a 'clean' MVVM solution. Hope I could give you some insights.
BTW, I don't know why you need this line count in the view-model...

Using attachedbehaviors with an HierarchicalDatatemplate

Currently I am trying to display a ObservableCollection of an custom class in a TreeView, when the user double clicks on a 'item' it will fire an method in the ViewModel passing the selected custom class as parameter. I am using the MVVM structure for my WPF Application.
The problem I am facing with this is that the Observable Collection is displayed with an HierarchicalDataTemplate. See underneath the whole XAML code for the TreeView
<TreeView Name="DeviceTreeView" ItemsSource="{Binding ViewableTIADeviceTree}" Grid.Column="3" Margin="5">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type treeviewable:ViewableTIADevice}" ItemsSource="{Binding DeviceItems}">
<TextBlock Text="{Binding Path=DeviceName}"/>
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="{x:Type treeviewable:ViewableTIADevice}">
<Setter Property="commandBehaviors:MouseDoubleClick.Command"
Value="{Binding TIADeviceTreeItemDoubleClick}"/>
<Setter Property="commandBehaviors:MouseDoubleClick.CommandParameter"
Value="{Binding}"/>
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type treeviewable:ViewableDeviceItem}">
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</TreeView.Resources>
</TreeView>
And the MouseDoubleClick attached behavior class:
public class MouseDoubleClick
{
public static DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command",
typeof(ICommand),
typeof(MouseDoubleClick),
new UIPropertyMetadata(CommandChanged));
public static DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter",
typeof(object),
typeof(MouseDoubleClick),
new UIPropertyMetadata(null));
public static void SetCommand(DependencyObject target, ICommand value)
{
target.SetValue(CommandProperty, value);
}
public static void SetCommandParameter(DependencyObject target, object value)
{
target.SetValue(CommandParameterProperty, value);
}
public static object GetCommandParameter(DependencyObject target)
{
return target.GetValue(CommandParameterProperty);
}
public static void CommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs args)
{
Control control = target as Control;
if(control != null)
{
if((args.NewValue != null) && (args.OldValue == null))
{
control.MouseDoubleClick += OnMouseDoubleClick;
}
else if((args.NewValue == null) && (args.OldValue != null))
{
control.MouseDoubleClick -= OnMouseDoubleClick;
}
}
}
private static void OnMouseDoubleClick(object sender, RoutedEventArgs e)
{
Control control = sender as Control;
ICommand command = (ICommand)control.GetValue(CommandProperty);
object commandParameter = control.GetValue(CommandParameterProperty);
command.Execute(commandParameter);
}
}
The problem I am facing with this is that it says that the 'ViewableTIADevice' is not an FrameWorkElement and thus I cannot even run it.
I've also tried using the
<Style TargetType"{x:Type TreeViewItem}">
That does run but I get no response when trying to double click an item in the TreeView.
I've searched a lot for the solution and I would like to refer to this thread: WPF/MVVM - how to handle double-click on TreeViewItems in the ViewModel?
I've been using the above thread as solution but how can I combine that solution with an HierarchicalDatatemplate?
EDIT
The ICommand that I am trying to call by double clicking an item
public RelayCommand TIADeviceTreeItemDoubleClick { get; set; }
Where I am here assigning it to the function
TIADeviceTreeItemDoubleClick = new RelayCommand(c => tiaDeviceTreeItemDoubleClick(c));
And the function it refers to:
private void tiaDeviceTreeItemDoubleClick(object value)
{
//code
}
This is the ViewableTIADevice class:
public class ViewableTIADevice
{
public ViewableTIADevice()
{
DeviceItems = new List<ViewableDeviceItem>();
}
public string DeviceName { get; set; }
public IList<ViewableDeviceItem> DeviceItems { get; set; }
}
i believe you have a missunderstanding of what your datacontext is, consider this example:
<TreeView ItemsSource="{Binding Items}">
<TreeView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ItemProperty1}"></TextBlock>
</DataTemplate>
</TreeView.ItemTemplate>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="Local:MouseDoubleClick.Command"
Value="{Binding ElementName=DeviceTreeView, Path=DataContext.TIADeviceTreeItemDoubleClick}"/>
<Setter Property="Local:MouseDoubleClick.CommandParameter"
Value="{Binding}"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
the datacontext of my TreeView, is my viewmodel, so when i say ItemsSource="{Binding Items}", i am binding to the observable collection called Items inside my ViewModel.
the datacontext inside TreeView.ItemTemplate, is one single item inside the collection Items. meaning that when i say {Binding ItemProperty1}, i am binding, NOT to ViewModel.ItemProprty1, but to a single item inside the collection ViewModel.Items. this means that you have to have a ViewModel, and inside a collection called Items, and inside this collection you need to have objects of type X, and the class X must have a property called ItemProperty1.
the datacontext inside the TreeView.ItemContainerStyle, is also one single item inside the collection Items, meaning that when you say {Binding TIADeviceTreeItemDoubleClick}, you are trying to bind to an ICommand property that is inside of the class that is inside your collection Items. your datacontext here, is not, as you assumed, your ViewModel, but rather one single item inside ViewModel.Items
so when you use this:
Value="{Binding ElementName=DeviceTreeView, Path=DataContext.TIADeviceTreeItemDoubleClick}"
you are binding to the datacontext of the TreeView, which is ViewModel, which contains an ICommand property called TIADeviceTreeItemDoubleClick.
when you write this:
{Binding TIADeviceTreeItemDoubleClick}
you are trying to bind to the datacontext of the current TreeViewItem, which is one single object inside your collection Items. so this will only work if you add the ICommand to your class that is inside your collection Items.
you also use this:
<Setter Property="Local:MouseDoubleClick.CommandParameter"
Value="{Binding}"
here, obviosly, you are sending one single item inside your collection Items, and not the ViewModel.
make sense?

custom user control binding on itemspaneltemplate

I'm having trouble binding to a custom dependencyproperty on a usercontrol to my MVVM ViewModel. My user control is correctly working when i use it directly on my view:
<local:CustomControl Mode="{Binding Mode, Mode=TwoWay}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="0">
<Button x:Name="InfoBox1" Content="Test1" />
<Button x:Name="InfoBox2" Content="Test2" />
</local:CustomControl>
But using it as an itemspaneltemplate the binding is not working:
<ItemsControl Grid.Row="0" ItemsSource="{Binding Equipment}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:CustomControl Mode="{Binding Mode, Mode=TwoWay}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
...
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I've tried using a RelativeSource and finding the itemscontrol/view and setting the path to either Mode or DataContext.Mode but I just can't get the binding to work.
Mode is defined as:
public static readonly DependencyProperty ModeProperty;
public Modes Mode
{
get { return (Modes)this.GetValue(ModeProperty); }
set { this.SetValue(ModeProperty, value); }
}
and registered in the constructor of the custom control:
public CustomControl()
{
Mode = Modes.Default;
}
static CustomControl()
{
ModeProperty = DependencyProperty.Register("Mode", typeof(Modes), typeof(CustomControl), new FrameworkPropertyMetadata(Mode.Default, OnModeChanged));
}
private static void OnModeChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
CustomControl ctrl= o as CustomControl ;
if (ctrl== null) return;
Modes mode = (Modes)e.NewValue;
ctrl.Mode = mode;
}
Do I need to use a workaround to get the control working as the panel template or am I just messing up the binding too bad?
----Edit
The viewmodel part:
private Modes _mode= Modes.Default;
public Modes Mode
{
get { return _mode; }
set { _mode= value; NotifyPropertyChanged(); }
}
private ObservableCollection<EquipmentViewModel> _equipment;
public ObservableCollection<EquipmentViewModel> Equipment
{
get { return _equipment; }
set { _equipment = value; NotifyPropertyChanged(); }
}
----Edit2:
I've investigated further and I'm more complexed. I've added the following to both the ItemsPanelTemplate's control and the one directly in the grid.
Visibility="{Binding Visible, Converter={StaticResource visibilityConverter}}"
Altering this Visible boolean works in both cases. So it appears to only be an issue with the custom DependencyProperty.
Inspecting the visual tree the DataContext of the control as ItemsPanelTemplate is also correct.
What could make the dependency property work properly when used straight and not when used as an itemspaneltemplate ?
Found what was causing the strange conflicting behavior.
I was setting the property to a certain value in the normal ctor
public CustomControl()
{
Mode = Modes.Default;
}
This was appearently causing a conflict when using the control as an Itemspaneltemplate. Removing this made the binding work as expected.
I guess the difference in behavior has something to do with the calls to the constructor at different times ?

How to group checkboxes in treeview wpf mvvm when selection range is [0,1]

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.

Categories