Reading the "IsExpanded" property from the ItemSource of a TreeViewItem - c#

So I want the IsExpanded Property of the TreeView Item so be reflected in the datacontext.
<TreeView x:Name="TreeViewMonitorEvents" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Column="0" Grid.Row="1" Grid.RowSpan="5"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Page}, Path=MonitorEventCatagories}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type tree:TreeGroup}" ItemsSource="{Binding Members}" >
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" MouseMove="DragDrop_MouseMove_TreeGroup">
<CheckBox Name="CheckboxTreeGroup" IsChecked="{Binding Path=(tree:TreeItemHelper.IsChecked), Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Template="{DynamicResource MonitoringUICheckBox}" Style="{StaticResource MonitoringUICheckBoxStyling}"
MouseMove="DragDrop_MouseMove_TreeGroup" Checked="CheckboxTreeGroup_Checked" Unchecked="CheckboxTreeGroup_Unchecked">
</CheckBox>
<TextBlock Text="{Binding Name}" Style="{StaticResource MonitorUIText}" MouseMove="DragDrop_MouseMove_TreeGroup"/>
</StackPanel>
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}" >
<Setter Property="IsExpanded" Value="{Binding IsExpanded,Mode=TwoWay}" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
I.e the TreeGroup Class. See Treegroup here:
namespace RTX64MonitorUtility.TreeView
{
//Data class. Holds information in the Datacontext section of the UI Elements it is attached to.
public class TreeGroup : DependencyObject, IParent<object>
{
public string Name { get; set; }
public List<TreeMonitoringEvent> Members { get; set; }
public IEnumerable<object> GetChildren()
{
return Members;
}
public bool IsExpanded { get; set; } = false;
}
}
Here is the List it's drawing from:
namespace RTX64MonitorUtility.Pages
{
/// <summary>
/// Interaction logic for EventsAndTriggers.xaml
/// </summary>
public partial class EventsAndTriggers : Page
{
public ObservableCollection<TreeGroup> MonitorEventCatagories { get; set; }
...
}
}
My primary goal here is that if the TreeGroup Item is not expanded then the chidren's Checked and Unchecked events do not get triggered so I need to do the required actions for them. If I can find out a way of reading the IsExpanded value from those events that would also be a solution.

You usually bind the item container's properties like TreeViewItem.IsExpanded or ListBoxItem.IsSelected etc. to your data model by using a Style that targets the item container. The DataContext of the item container is the data model:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
Next, implement the data model properly. The properties that are the source of the data binding must be implemented as dependency properties (for dependency objects) or raise INotifyPropertyChanged.PropertyChanged event.
Since DependencyObject extends DispatcherObject, any class that extends DependencyObject is thread affine: in WPF, a DispatcherObject can only be accessed by the Dispatcher it is associated with.
For this reason you usually prefer INotifyPropertyChanged on data models.
DependencyObject is for UI objects that are bound to the Dispatcher thread by definition.
// Let the model implement the INotifyPropertyChanged interface
public class TreeGroup : INotifyPropertyChanged, IParent<object>
{
// Follow this pattern for all properties that will change
// and are source of a data binding.
private bool isExpanded;
public IsExpanded
{
get => this.isExpanded;
set;
{
this.isExpanded = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
=> this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
See Microsoft Docs: Data binding overview (WPF .NET)

Related

WPF Binding not updating XAML but PropertyChanged called

I am trying to bind the visibility of a WPF UI element in my XAML to a property of my view model (MainViewModel.DisplayPopup), which is updated from the property of another view model (ContactViewModel) which is a property of this class (MainViewModel). The BaseViewModel class extends INotifyPropertyChanged and uses a Nuget package that automatically calls the PropertyChanged event inside of each property's setter.
This is my view model code:
public class MainViewModel : BaseViewModel
{
public ObservableCollection<ContactViewModel> TankItems {get; set; }
public bool DisplayPopup
{
get => TankItems.Any(contact => contact.DisplayPopup);
}
public MainViewModel() : base()
{
TankItems = new ObservableCollection<ContactViewModel>();
TankItems.Add(new ContactViewModel());
TankItems.Add(new ContactViewModel());
}
}
public class ContactViewModel : BaseViewModel
{
private bool _isSelected = false;
public bool DisplayPopup {get; set; } = false;
public bool IsSelected {get => _isSelected; set { _isSelected = value; DisplayPopup = value;
}
This is my XAML:
<ListBox Grid.Column="0" ItemsSource="{Binding TankItems}" SelectionChanged="List_SelectionChanged">
<ListBox.Style>
<Style TargetType="ListBox">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Style>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="Test" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border BorderBrush="Orange" BorderThickness="3" Grid.Column="1">
<TextBlock Text="{Binding DisplayPopup}" /> <!-- Stays as false -->
</Border>
What I expect to happen is when I click one of the ListBox items, DisplayPopup becomes true, but it stays the same. HOWEVER, if I log the value of ((MainViewModel)DataContext).DisplayPopup) I get the correct value - false in the beginning then true when the selection is changed.
Why is the binding value not updating?
Update
Here is the BaseViewModel, which uses Fody PropertyChanged
[AddINotifyPropertyChangedInterfaceAttribute]
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (sender, e) => { };
public void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
}
The reason why the binding to DisplayPopup does not update is that the property is a computed property. A computed property lacks a setter and therefore never raises INotifyPropertyChanged.PropertyChanged. The data binding listens to this event. As the result DisplayPopup.Get is only called once (the moment the binding is initialized).
To solve this you can either let the MainViewModel listen to PropertyChanged events of the ContactViewModel items or as it seems that you are interested in selected items simply bind the ListBox.SelectedItem and change MainViewModel.DisplayPopup on changes.
For simplicity I recommend the second solution.
Note that in order to make the ListBox.IsSelected binding work you must set the ListBox.ItemContainerStyle and target ListBoxItem instead of ListBox:
MainViewModel.cs
public class MainViewModel : BaseViewModel
{
public ObservableCollection<ContactViewModel> TankItems { get; set; }
private ContactViewModel selectedTankItem;
public ContactViewModel SelectedTankItem
{
get => this.selectedTankItem;
set
{
this.selectedTankItem = value;
OnPropertyChanged(nameof(this.SelectedTankItem));
this.DisplayPopup = this.SelectedTankItem != null;
}
// Raises INotifyPropertyChanged.PropertyChanged
public bool DisplayPopup { get; set; }
public MainViewModel() : base()
{
TankItems = new ObservableCollection<ContactViewModel>()
{
new ContactViewModel(),
new ContactViewModel()
};
}
}
MainWindow.xaml
<ListBox ItemsSource="{Binding TankItems}"
SelectedItem="{Binding SelectedTankItem}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type ContactViewModel}">
<StackPanel>
<TextBlock Text="Test" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Border>
<TextBlock Text="{Binding DisplayPopup}" />
</Border>
Binding Selected State in Tank Items
There are multiple issues in your code. Let's start with the ContactViewModel.
public bool IsSelected {get => _isSelected; set { _isSelected = value; DisplayPopup = value;
The DisplayPopup property is redundant, as it is has the same state as IsSelected, remove it.
<ListBox.Style>
<Style TargetType="ListBox">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ListBox.Style>
This XAML is wrong. You have to bind the IsSelected property of a ListBoxItem, not the ListBox. Plus you bind to an IsSelected property on MainViewModel, as this is the DataContext here. This property does not exist, so the binding will not work. Instead use an ItemContainerStyle.
<ListBox.ItemContainerStyle>SelectedTankItem
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
Updating the Display Popup Property
You want to set DisplayPopup on MainViewModel to true, if any item is selected. The item container style above will set the IsSelected property of your ContactViewModel, but it does not automatically trigger a property change of DisplayPopup in MainViewModel. Hence, the "Text" binding will never update its value.
To solve this, make the DisplayPopup property in MainViewModel a simple get-set property. You do not need to compute it. Create a second property to bind the SelectedItem of the ListBox in MainViewModel. This property will get set, when the selection changes.
public bool DisplayPopup { get; set; }
public ContactViewModel SelectedTankItem { get; set; }
Additionally, create a method called OnSelectedTankItemChanged where you set the DisplayPopup property depending on SelectedTankItem. This method will automatically be called by the Fody framework, when SelectedTankItem changes.
public void OnSelectedTankItemChanged()
{
DisplayPopup = SelectedTankItem != null;
}
Then bind the SelectedItem on your ListBox to SelectedTankItem.
<ListBox Grid.Column="0" ItemsSource="{Binding TankItems}" SelectedItem="{Binding SelectedTankItem}">
<!-- ...other code. -->
</ListBox>
You can simplify your base view model by removing the property changed code. You do not need it, since the attribute will cause Fody to implement it for you.
[AddINotifyPropertyChangedInterfaceAttribute]
public class BaseViewModel : INotifyPropertyChanged
{
}

How can I bind class fields to elements inside custom templates ? (WPF)

How can I bind class fields to elements inside a custom ListBoxItem template??
This sample code is lacking some aspects like class definition, ListBox name etc., I just struggle with the binding process.
<ListBox>
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<!-- how to bind class fields to these elements -->
<StackPanel Orientation="Horizontal">
<Label></Label>
<Image></Image>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
<!-- adding items dynamically -->
</ListBox>
C# code should look like:
ListBoxItem listBoxItem = new ListBoxItem();
listBoxItem.Content = new MyClass(){ Name="MyName", Image="ImagePath"}
... append to ListBox ...
When developing in WPF MvvM is being encouraged to promote separation of concerns. To do so one would implement a View Model with Properties that then would be bound to the view. When you want the UI (view) to be aware of the changes to the data that View Model provides one has to implement the INotifyPropertyChanged interface, like so:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows.Threading;
namespace ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public BaseViewModel()
{
//ctor
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
UIThread( () =>
{
//make sure the event is raised on the main thread
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
});
}
Dispatcher _dispacther;
protected void UIThread(Action action)
{
if (_dispacther == null)
{
_dispacther = Dispatcher.CurrentDispatcher;
}
_dispacther.Invoke(action);
}
}
}
Your Hello class would derive from the BaseViewModel and provide OnPropertyChanged(); in the property like this:
private string name;
public string Name
{
get { return name; }
set { name= value; OnPropertyChanged(); }
}
Now when you change a name of the selected Item in the ListBox it will be reflected in the UI. Try it!
This implementation ensures that the event is raised on main thread, so you don't need to do it in when invoking the event. Also [CallerMemberName] will populate the name of the property that you invoke!
Figured it out. C# class:
public class Hello
{
public string Name { get; set; }
public string Description { get; set; }
public string ImagePath { get; set; }
}
C# dynamic adding:
ListBoxItem lbi = new ListBoxItem();
lbi.Content = new Hello() { Name = "hello man", Description="I am superman.", ImagePath="Images/myimage.png"};
menu.Items.Add(lbi);
XAML:
<ListBox Name="menu" ItemsSource="{Binding}">
<ListBox.Resources>
<Style TargetType="ListBoxItem">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label FontWeight="Bold" Content="{Binding Name}"></Label>
<Label Content="{Binding Description}"></Label>
<Image Source="{Binding ImagePath}" Width="30" Height="30"></Image>
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
</ListBox>

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?

Navigate through TabItem using MVVm

Assum that I have 3 user Control(TIShowNames,TIEnterCode,TIShowFactor).
they have their views and their corresponding viewModel.
all these 3, are in mainwindowView.
Here is my mainwindowView Xaml:
<Controls:TransitionPresenter Name="transContainer" Grid.Row="2" RestDuration="0:0:1" IsLooped="False" Transition="{StaticResource SlideTransition}">
<TabControl Name="TCMain" Background="#00FFFFFF" BorderThickness="0" Padding="0 -5 0 0 ">
<TabItem Name="TIShowNames" Visibility="Collapsed">
<views:NameView x:Name="NameViewElement" />
</TabItem>
<TabItem Name="TIEnterCode" Visibility="Collapsed">
<views:CodeView x:Name="CodeViewElement" />
</TabItem>
<TabItem Name="TIShowFactor" Visibility="Collapsed">
<views:FactorDetailView x:Name="FactorDetailViewElement" />
</TabItem>
</TabControl>
</Controls:TransitionPresenter>
In my old Programming style i used to use this line of code for navigating through tab items(without any pattern):
private void ChangeTabItemTo(TabItem TI)
{
transContainer.ApplyTransition("TCMain", "TCMain");
TCMain.SelectedItem = TI;
}
I have a btn show in "TIShowNames", so when i clicks on that it has to go to "TIShowFactor".
In MVVM, ViewModel does not know any thing about view(this item tab is in its parent view!!!). so how he can change selected Tab Item without violating MVVM??
Another Try:
Changing Selectedindex wont work because of this error:
"System.Windows.Data Error: 40 : BindingExpression path error: 'Index'
property not found on 'object' ''MainWindowViewModel'
(HashCode=22018304)'. BindingExpression:Path=AAA;
DataItem='MainWindowViewModel' (HashCode=22018304); target element is
'TabControl' (Name=''); target property is 'IsSelected' (type
'Boolean')"
Update:
Controls:TransitionPresenter is from Fluid DLL
Update:
I want to hide tab item's header so no one can click the header and navigatoin through header is possibe only via btns in usercontrols
You could define a DataTemplate per view model type in the view:
<TabControl Name="TCMain"
ItemsSource="{Binding ViewModels}"
SelectedItem="{Binding ViewModel}"
Background="#00FFFFFF" BorderThickness="0" Padding="0 -5 0 0 ">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:NameViewViewModel}">
<views:NameView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:CodeViewViewModel}">
<views:CodeView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:FactorDetailViewModel}">
<views:FactorDetailView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
...and bind the SelectedItem property to a source property that you set in your view model, e.g.:
public object ViewModel
{
get { return _vm; }
set { _vm = value; NotifyPropertyChanged(); }
}
...
ViewModel = new CodeViewViewModel(); //displays the CodeView
Expanding on mm8's answer, this is how I'd do it:
First of all, I would create a BaseViewModel class to be inherited by every view model that will represent each tab of the TabControl.
I like to implement it as an abstract class with an abstract string property called "Title", so I can dynamically create the tabs and display their names (or titles). This class would also implement the NotifyPropertyChanged interface.
public abstract class BaseViewModel : INotifyPropertyChanged
{
public abstract string Title { get; }
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Then I would create each view model inheriting from this base view model. for example:
public class NameViewModel : BaseViewModel
{
public override string Title
{
get
{
return "Name";
}
}
}
You would do the same for the other view models, only changing the "title" property of each of them.
Now I would create the MainView of the application and its corresponding view model.
The MainViewModel would have a collection of BaseViewModels and a "CurrentViewModel" (of type BaseViewModel) and would add all the view models you want to its collection on its constructor, like this:
public class MainViewModel : BaseViewModel
{
public override string Title
{
get
{
return "Main";
}
}
private ObservableCollection<BaseViewModel> _viewModels;
public ObservableCollection<BaseViewModel> ViewModels
{
get { return _viewModels; }
set
{
if (value != _viewModels)
{
_viewModels = value;
OnPropertyChanged();
}
}
}
private BaseViewModel _currentViewModel;
public BaseViewModel CurrentViewModel
{
get { return _currentViewModel; }
set
{
if (value != _currentViewModel)
{
_currentViewModel = value;
OnPropertyChanged();
}
}
}
public MainViewModel()
{
ViewModels = new ObservableCollection<BaseViewModel>();
ViewModels.Add(new NameViewModel());
ViewModels.Add(new CodeViewModel());
ViewModels.Add(new FactorDetailViewModel());
}
}
Finally, your main view would be similar to what mm8 posted:
(Notice the differences from my code to mm8's code: (1) You need to set the DisplayMemberPath of the TabControl to the "Title" property of the BaseViewModels and (2) You need to set the DataContext of the Window to your MainViewModel)
<Window ...>
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<TabControl Name="TCMain"
ItemsSource="{Binding ViewModels}"
DisplayMemberPath="Title"
SelectedItem="{Binding CurrentViewModel}"
Background="#00FFFFFF" BorderThickness="0" Padding="0 -5 0 0 ">
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:NameViewModel}">
<local:NameView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:CodeViewModel}">
<local:CodeView />
</DataTemplate>
<DataTemplate DataType="{x:Type local:FactorDetailViewModel}">
<local:FactorDetailView />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Window>
Now it should work as expected. Everytime you change the active tab of the TabControl, the SelectedItem property of the control will change to the corresponding view model, which will be templated as its corresponding view.
This approach is called "View Model First" (instead of View First), by the way.
EDIT
If you want to have a button on one of the view models that has a command to change the current view model, this is how you do it:
I suppose you are familiarized with Josh Smith's RelayCommand. If you are not, just search for its implementation on the web.
You will need to create an ICommand property on your MainViewModel, which will be responsible to change the "CurrentViewModel" property:
private ICommand _showFactorDetailCommand;
public ICommand ShowFactorDetailCommand
{
get
{
if (_showFactorDetailCommand == null)
{
_showFactorDetailCommand = new RelayCommand(p => true, p => show());
}
return _showFactorDetailCommand;
}
}
private void show()
{
CurrentViewModel = ViewModels.Single(s => s.Title == "Factor");
}
The show() method above simply searches the collection of view models that has the title "Factor" and set it to the CurrentViewModel, which in turn will be the Content of the ContentControl that acts as the ContentTemplate of your TabControl inside your main view.
Remember that your FactorDetailViewModel should be implemented as follows:
public class FactorDetailViewModel : ViewModelBase
{
public override string Title
{
get
{
return "Factor";
}
}
}
The button inside your "NameView" will bind to this command which is a property of "MainViewModel" using RelativeSource binding:
<Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ShowFactorDetailCommand}" Content="Show Factor" Height="20" Width="60"/>
You could make this command more generic, passing the title of the view model you would like to navigate to as the command parameter:
private ICommand _showCommand;
public ICommand ShowCommand
{
get
{
if (_showCommand == null)
{
_showCommand = new RelayCommand(p => true, p => show(p));
}
return _showCommand;
}
}
private void show(p)
{
var vm = (string)p;
CurrentViewModel = ViewModels.Single(s => s.Title == vm);
}
Then on your views, pass the Command Parameter too:
<Button Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}, Path=DataContext.ShowCommand}" Content="Show Factor" CommandParameter="Factor" Height="20" Width="60"/>
Finally, to hide your TabItems completely, you need to set the ItemContainerStyle of your TabControl so that the Visibility of your TabItems has the value of "Collapsed".
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
</TabControl.ItemContainerStyle>

List Box items with checkboxes, multiselect not working properly in WPF MVVM

So I have a ListBox with CheckBox-es that have the IsChecked property bound to the Item's property called IsSelected. That produces a weird behavior where if I click on the item itself it checks the checkbox (good) and sets the property on the item (good), but doesn't actually select the item in the list box, ie. the highlighting isn't there. I am guessing that the ListBox IsSelected property needs to be set as well for that right?
Now, I am trying to get the multi-select behavior to work so I changed the SelectionMode to Extended. Now, I can select only Items, not the checkboxes. What happens is that if I use SHIFT + click by pointing at the area next to the item, not the item itself, then it select multiple items, but clicking on the items themselves doesn't do the trick of multi-selection not does it check the checkboxes. What is going on in here?
I would like to be able to select multiple items by holding shift etc, and have that trigger the property on the Elevation item so I know which ones are checked. Any help is appreciated.
Here's my XAML:
<ListBox x:Name="LevelsListBox"
ItemsSource="{Binding Elevations, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended"
BorderThickness="0">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected}" Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
My View Model:
public class AxoFromElevationViewModel : ViewModelBase
{
public AxoFromElevationModel Model { get; }
public RelayCommand CheckAll { get; }
public RelayCommand CheckNone { get; }
public AxoFromElevationViewModel(AxoFromElevationModel model)
{
Model = model;
Elevations = Model.CollectElevations();
CheckAll = new RelayCommand(OnCheckAll);
CheckNone = new RelayCommand(OnCheckNone);
}
private void OnCheckNone()
{
foreach (var e in Elevations)
{
e.IsSelected = false;
}
}
private void OnCheckAll()
{
foreach (var e in Elevations)
{
e.IsSelected = true;
}
}
/// <summary>
/// All Elevation Wrappers.
/// </summary>
private ObservableCollection<ElevationWrapper> _elevations = new ObservableCollection<ElevationWrapper>();
public ObservableCollection<ElevationWrapper> Elevations
{
get { return _elevations; }
set { _elevations = value; RaisePropertyChanged(() => Elevations); }
}
}
Finally my Elevation Class:
public sealed class ElevationWrapper : INotifyPropertyChanged
{
public string Name { get; set; }
public ElementId Id { get; set; }
public object Self { get; set; }
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set { _isSelected = value; RaisePropertyChanged("IsSelected"); }
}
public ElevationWrapper(View v)
{
Name = v.Name;
Id = v.Id;
Self = v;
IsSelected = false;
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propname)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propname));
}
}
You should bind the IsSelected property of your ListBoxItems to the IsSelected property of your view model. This way CheckBoxes will trigger the selection and when you select an item, the related CheckBox will be checked.
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
It seems to me you want to sync 3 properties ListBoxItem.IsSelected, CheckBox.IsChecked and your models IsSelected.
My advice is that only one of the templates/styles should bind to the underlying model so I will add Yusuf answer as I will use the ListBoxItem style to bind to your model property.
After that you should bind the Checkbox.IsChecked to the ListBoxItem.IsSelected and your ListBox should look like this:
<ListBox x:Name="LevelsListBox"
ItemsSource="{Binding Elevations, UpdateSourceTrigger=PropertyChanged}"
SelectionMode="Extended"
BorderThickness="0">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding IsSelected, RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}" Content="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Always try to bind XAML properties in a chain way, e.g. model.A binds to Model.B binds to Model.C, doing this should help you keep updates consistent and avoid wierd cases.
There is an issue with this code though, after you select multiple items and click one check box it will only unselect that item but if you click another item it will unselect all except that item.

Categories