I am creating side menu using MasterDetailPage and the list of menu items is implemented using ListView. I want to make custom appearance for selected item:
Background color
Label's text color
ImageSource for icon
How can I do this?
I create field IsActive in my list view item and use DataTrigger binded to this field and set all properties what I need.
In XAML
I set color of selected item to the StackLayout, so I just hide original selected color of ListView (orange in my case on Android)
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Orientation="Horizontal"
Padding="16, 0, 16, 0"
HeightRequest="48">
<StackLayout.Triggers>
<DataTrigger TargetType="StackLayout"
Binding="{Binding IsActive}"
Value="True">
<Setter Property="BackgroundColor" Value="{StaticResource menu-background-active}"/>
</DataTrigger>
</StackLayout.Triggers>
<Image Source="{Binding IconSource}"
VerticalOptions="Center"
WidthRequest="20"/>
<Label Text="{Binding Title}" TextColor="Black"
VerticalOptions="Center"
Margin="36,0,0,0">
<Label.Triggers>
<DataTrigger TargetType="Label"
Binding="{Binding IsActive}"
Value="True">
<Setter Property="TextColor" Value="{StaticResource menu-text-active}"/>
</DataTrigger>
</Label.Triggers>
</Label>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
ItemsSource item
Note that this class should implement INotifyPropertyChanged
public class MasterPageItem : INotifyPropertyChanged
{
private string _title;
private string _iconSource;
private bool _isActive;
public string Title
{
get { return _title; }
set
{
_title = value;
OnPropertyChanged(nameof(Title));
}
}
/// <summary>
/// Set or return icon file
/// If IsActive == true
/// will add suffix "_active" to return value,
///
/// Note:
/// Icons file should be the pair"
/// - icon_name.png
/// - icon_name_active.png
///
/// </summary>
public string IconSource
{
get
{
if (!IsActive)
return _iconSource;
return Path.GetFileNameWithoutExtension(_iconSource) + "_active" + Path.GetExtension(_iconSource);
}
set
{
_iconSource = value;
OnPropertyChanged(nameof(IconSource));
}
}
/// <summary>
/// Is menu item is selected
/// </summary>
public bool IsActive
{
get { return _isActive; }
set
{
_isActive = value;
OnPropertyChanged());
}
}
public Type TargetType { get; set; }
// Important for data-binding
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string prop = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
Then in the master page code behind I use ItemSelected event to
change the IsActive property
private void ListView_ItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as MasterPageItem;
var items = listView.ItemsSource as IList<MasterPageItem>;
//Select current item and deselect others
for(int i = 0; i<items.Count; i++)
items[i].IsActive = items[i] == item;
if (item != null)
{
ItemSelected?.Invoke(this, item.TargetType);
_activePage = item.TargetType;
}
}
Related
I made quite research to implement selected items via MVVM in WPF. I thought I had success but now the selection is made according to scroll position. I select all items in the listbox but only first 11 marked as selected. If I scroll more, more selected. If I scroll to the bottom all items selected. Is there solution for this problem?
XAML:
<ListBox x:Name="DataListBox" SelectionMode="Extended" HorizontalAlignment="Left" Margin="5,5,0,0" Grid.Row="1" Grid.Column="0" Grid.RowSpan="8"
VerticalAlignment="Top" Height="200" Width="200"
ItemsSource="{Binding DataListBoxItemsSource, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding DataListBoxSelectedItem, UpdateSourceTrigger=PropertyChanged}"
>
<ListBox.InputBindings>
<KeyBinding Command="ApplicationCommands.SelectAll" Modifiers="Ctrl" Key="A" />
</ListBox.InputBindings>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:CallMethodAction TargetObject="{Binding}" MethodName="DataListBox_SelectionChanged"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
View Model
public async void CreateLayersTOC()
{
if (MapView.Active != null)
{
if (DataListBoxSelectedItem != null || FavoriteTabsSelectedItem != null)
MainStackPanelIsEnabled = false;
LayerNames = new List<string>();
await Task.Run(() =>
{
MessageBox.Show("source count " + DataListBoxItemsSource.Count);//58 items all selected
if (DataListBoxSelectedItem != null)
foreach (ItemPresenter itemP in DataListBoxItemsSource)
{
if (itemP.IsSelected)
{
if (LayerNames.Contains(itemP.ToString()) == false)
LayerNames.Add(itemP.ToString());
}
}
if (FavoriteTabsSelectedItem != null)
{
foreach (ItemPresenter itemP in FavListBoxItemsSource)
{
if (itemP.IsSelected)
{
if (LayerNames.Contains(itemP.ToString()) == false)
LayerNames.Add(itemP.ToString());
}
}
}
MessageBox.Show("Coll" + LayerNames.Count);//Count depends on scroll position
});
//do stuff
}
else
MessageBox.Show("Make sure to have a map available before adding layers to a map");
MainStackPanelIsEnabled = true;
}
public class ItemPresenter : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private readonly string _value;
public ItemPresenter(string value)
{
_value = value;
}
public override string ToString()
{
return _value;
}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
OnPropertyChanged();
}
}
}
}
Here is a simple example of everything that is necessary to bind the SelectedItem of a ListBox and the IsSelected property of the ListBoxItems.
XAML:
<ListBox ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
SelectionMode="Extended">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding Selected}"/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
View Model:
public class DataItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public string Name { get; set; }
private bool selected;
public bool Selected
{
get { return selected; }
set
{
selected = value;
PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(nameof(Selected)));
Debug.WriteLine(Name + " selected: " + selected);
}
}
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<DataItem> Items { get; }
= new ObservableCollection<DataItem>();
public IEnumerable<DataItem> SelectedItems
{
get { return Items.Where(i => i.Selected); }
}
private DataItem selectedItem;
public DataItem SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
}
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new ViewModel();
vm.Items.Add(new DataItem { Name = "Item 1" });
vm.Items.Add(new DataItem { Name = "Item 2" });
vm.Items.Add(new DataItem { Name = "Item 3" });
vm.Items.Add(new DataItem { Name = "Item 4" });
vm.Items.Add(new DataItem { Name = "Item 5" });
DataContext = vm;
}
}
In a WPF window I show a treeview with checkboxes with disks/directories on a Pc. When the user expands a node, an event calls folder_Expanded adding the subdirectories of that node.
What should happen is that certain directories show a color (this works) and certain directories are checked if they are found in a XML file. The user can then check or uncheck (sub)directories after which the modified directory selection is again stored in that xml file.
However, I can't get a checkbox in that treeviewitem checked with a certain directory. In the code of the expanded event, I test it with a sample directory. The background color works fine, but the IsSelected line is doing nothing. Reason is that PropertyChanged is null so it doesn't create an instance of PropertyChangedEventArgs. I would say I have everything: a model inheriting from INotifyPropertyChanged and assigned as DataContext in the XAML and setting the property IsChecked of the CheckBox as defined in the XAML via this model.
What do I miss?
Alternatively I would like to know if I can directly set the checkbox to checked, without databinding, like I set the background color? Problem with databinding is when it doesn't work there's no way to debug the code, it just doesn't work....
At the start:
SelectFilesModel selectFilesModel = new SelectFilesModel();
public SelectFiles()
{
InitializeComponent();
Window_Loaded();
}
void folder_Expanded(object sender, RoutedEventArgs e)
{
TreeViewItem item = (TreeViewItem)sender;
if (item.Items.Count == 1 && item.Items[0] == dummyNode)
{
item.Items.Clear();
try
{
foreach (string s in Directory.GetDirectories(item.Tag.ToString()))
{
TreeViewItem subitem = new TreeViewItem();
subitem.Header = s.Substring(s.LastIndexOf("\\") + 1);
subitem.Tag = s;
subitem.FontWeight = FontWeights.Normal;
subitem.Items.Add(dummyNode);
subitem.Expanded += new RoutedEventHandler(folder_Expanded);
if (s.ToLower() == "c:\\temp") // Sample directory to test
{
subitem.Background = Brushes.Yellow; // This works!
selectFilesModel.IsChecked = true; // Eventually PropertyChanged is always null!!
}
item.Items.Add(subitem);
}
}
catch (Exception e2)
{
MessageBox.Show(e2.Message + " " + e2.InnerException);
}
}
}
The XAML looks as follows:
<Window.DataContext>
<local:SelectFilesModel/>
</Window.DataContext>
<Grid>
<TreeView x:Name="foldersItem" SelectedItemChanged="foldersItem_SelectedItemChanged" Width="Auto" Background="#FFFFFFFF" BorderBrush="#FFFFFFFF" Foreground="#FFFFFFFF">
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Image Name="img" Width="20" Height="20" Stretch="Fill"
Source="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=Header,
Converter={x:Static local:HeaderToImageConverter.Instance}}"
/>
<TextBlock Name="DirName" Text="{Binding}" Margin="5,0" />
<CheckBox Name="cb" Focusable="False" IsThreeState="True" IsChecked="{Binding IsChecked ,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center"/> </StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</TreeView.Resources>
</TreeView>
</Grid>
and the model looks as follows:
public class SelectFilesModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
bool? _isChecked = false;
public bool? IsChecked
{
get { return _isChecked; }
set { this.SetIsChecked(value, true, true); }
}
void SetIsChecked(bool? value, bool updateChildren, bool updateParent)
{
if (value == _isChecked)
return;
_isChecked = value;
RaisePropertyChanged("IsChecked");
}
void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(prop)); }
}
} // SelectFilesModel
It would be interesting to see how youuu initialize the TreeView. It really looks like the selectFilesModel is not source of any data binding. It's not even part of your tree.
You are adding TreeViewItem manually (which is not a good idea - see your problem, which wouldn't exist if you would focus on dealing with the data models instead). Because of adding TreeViewItem elements directly, the DataContext of the TreeViewItem is the item itself.
The DataContext of your HeaderTemplate is the header value, which in your case is a string. You see selectFilesModel is never involved.
CheckBox.IsChecked currently binds to this string and we all know string has no property IsChecked.
What you should do is to create the tree using SelectFilesModel.
The following example is your modified code. It is not tested and written with no editor so it may contain minor erros. It should be enough to show the pattern.
Also note that Directory.EnumerateDirectories will perform much better in your scenario than Directory.GetDirectories.
Create an enum to express different states. Each state will map to a color which you set in XAML using a trigger.
enum DirectoryState
{
Default = 0,
Special
}
Then modify SelectFilesModel to allow to reference its children (subdirectories) and add a State enum property
public class SelectFilesModel : INotifyPropertyChanged
{
// TODO::Implement constructor to initialize properties
public event PropertyChangedEventHandler PropertyChanged;
bool? _isChecked = false;
public bool? IsChecked
{
get { return _isChecked; }
set { this.SetValue(value, ref _isChecked, true, true); }
}
DirectoryState _state;
public DirectoryState State
{
get { return _state; }
set { this.SetValue(value, ref _state, true, true); }
}
string _path;
public string Path
{
get { return _path; }
set { this.SetValue(value, ref _path, true, true); }
}
public ObservableCollection<SelectFilesModel> Subdirectories { get; }
void SetValue<TValue>(TValue value, ref TValue field, bool updateChildren, bool updateParent, [CallerMemberName] string propertyName = null)
{
if (value == field)
return;
field = value;
RaisePropertyChanged(propertName);
}
void RaisePropertyChanged(string prop) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
Then build the tree using the model. Note that since Expanded is a routed event, you don't have to subscribe to each item explicitly. Just listen to the routed event.
ObservableCollection<SelectFilesModel> TreeRoot { get; }
public SelectFiles()
{
InitializeComponent();
Window_Loaded();
foldersItem.AddHandler(TreeViewItem.ExpandedEvent, new RoutedEventHandler(folder_Expanded)));
TreeRoot = new ObservableCollection<SelectFilesModel>() { new SelectFilesModel() };
foldersItem.ItemsSource = TreeRoot;
}
void folder_Expanded(object sender, RoutedEventArgs e)
{
var item = (sender as TreeViewItem).DataContext as SelectFilesModel;
if (item.Subdirectories.Count == 1 && item.Subdirectories[0] == dummyNode)
{
item.Subdirectories.Clear();
try
{
foreach (string s in Directory.EnumerateDirectories(item.Path))
{
var subitem = new SelectFilesModel() { Path = Path.GetDirectoryName(s) };
subitem.Subdirectories.Add(dummyNode);
if (subitem.Path.ToLower() == "c:\\temp") // Sample directory to test
{
subitem.State = DirectoryState.Special; // This works!
subitem.IsChecked = true; // This should work too
}
item.Subdirectories.Add(subitem);
}
}
catch (Exception e2)
{
MessageBox.Show(e2.Message + " " + e2.InnerException);
}
}
}
Finally define the data temnplate with the appropriate triggers and add it to e.g. TreeView.Resources:
<HierarchicalDataTemplate DataType="{x:Type SelectFilesModel}
ItemsSource="{Binding Subdirectories}">
<StackPanel Orientation="Horizontal">
<Image Name="img" Width="20" Height="20" Stretch="Fill"
Source="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type TreeViewItem}},
Path=Header,
Converter={x:Static local:HeaderToImageConverter.Instance}}"
/>
<TextBlock Name="DirName" Text="{Binding Path}" Margin="5,0" />
<CheckBox Name="cb" Focusable="False" IsThreeState="True" IsChecked="{Binding IsChecked ,UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center"/> </StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding State}" Value="{x:Static DirectoryState.Special}">
<Setter TargetName="DirName" Property="Foreground" Value="Yellow" />
</DataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
What I simply want to achieve is to change expand/collapse state of all TreeViewItems from Code Behind. I have created two event handlers for two buttons:
private void Button_Click(object sender, RoutedEventArgs e)
{
for(int i=0;i<trv.Items.Count;i++)
{
TreeViewItem item = (TreeViewItem)(trv.ItemContainerGenerator.ContainerFromIndex(i));
item.IsExpanded = false;
}
}
private void Button_Click1(object sender, RoutedEventArgs e)
{
for (int i = 0; i < trv.Items.Count; i++)
{
TreeViewItem item = (TreeViewItem)(trv.ItemContainerGenerator.ContainerFromIndex(i));
item.IsExpanded = true;
}
}
And my TreeView part of XAML:
<TreeView Name="trv" ItemsSource="{Binding modelItems}">
<TreeView.ItemTemplate>
<DataTemplate>
<TreeViewItem ItemsSource="{Binding modelSubItems}">
<TreeViewItem.Header>
<Grid Width="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding itemId}"/>
<TextBox Text="{Binding itemName}"/>
</TreeViewItem.Header>
<TreeViewItem.ItemTemplate>
<DataTemplate>
<Grid Margin="-20,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding subItemId}"/>
<TextBox Text="{Binding subItemName}"/>
</Grid>
</DataTemplate>
<TreeViewItem.ItemTemplate>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
This does not work, TreeView items do not react on IsExpanded changing from Code Behind.
Many sources say that the problem is in DataTemplate. So, I have changed my XAML adding TreeView.ItemContainerStyle:
<TreeView Name="trv" ItemsSource="{Binding modelItems}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<DataTemplate>
<TreeViewItem ItemsSource="{Binding modelSubItems}">
<TreeViewItem.Header>
<Grid Width="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding itemId}"/>
<TextBox Text="{Binding itemName}"/>
</TreeViewItem.Header>
<TreeViewItem.ItemTemplate>
<DataTemplate>
<Grid Margin="-20,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"></ColumnDefinition>
<ColumnDefinition Width="50"></ColumnDefinition>
</Grid.ColumnDefinitions>
<TextBox Text="{Binding subItemId}"/>
<TextBox Text="{Binding subItemName}"/>
</Grid>
</DataTemplate>
<TreeViewItem.ItemTemplate>
</TreeViewItem>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And now. Where should I place IsExpanded definition? In ModelView, in Model? I have tried both, no luck. When placed in ModelView I am getting in output:
System.Windows.Data Error: 40 : BindingExpression path error: 'IsExpanded' property not found on 'object' ''modelItems' (HashCode=43304686)'. BindingExpression:Path=IsExpanded; DataItem='modelItems' (HashCode=43304686); target element is 'TreeViewItem' (Name=''); target property is 'IsExpanded' (type 'Boolean')
When placed in Model, no Binding errors, but still doesn't work.
Of course in both (Model and ModelView), I have INotifyPropertyChanged implemented, which generally works:
public class ModelItem : INotifyPropertyChanged
{
(...)
public event PropertyChangedEventHandler PropertyChanged;
(...)
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
The way I handle WPF TreeView controls in a MVVM context is through the use of a common base class to hold the data for each node.
/// <summary>
/// A base class for items that can be displayed in a TreeView or other hierarchical display
/// </summary>
public class perTreeViewItemViewModelBase : perViewModelBase
{
// a dummy item used in lazy loading mode, ensuring that each node has at least one child so that the expand button is shown
private static perTreeViewItemViewModelBase LazyLoadingChildIndicator { get; }
= new perTreeViewItemViewModelBase { Caption = "Loading Data ..." };
private bool InLazyLoadingMode { get; set; }
private bool LazyLoadTriggered { get; set; }
private bool LazyLoadCompleted { get; set; }
private bool RequiresLazyLoad => InLazyLoadingMode && !LazyLoadTriggered;
// Has Children been overridden (e.g. to point at some private internal collection)
private bool LazyLoadChildrenOverridden => InLazyLoadingMode && !Equals(LazyLoadChildren, _childrenList);
private readonly perObservableCollection<perTreeViewItemViewModelBase> _childrenList
= new perObservableCollection<perTreeViewItemViewModelBase>();
/// <summary>
/// LazyLoadingChildIndicator ensures a visible expansion toggle button in lazy loading mode
/// </summary>
protected void SetLazyLoadingMode()
{
ClearChildren();
_childrenList.Add(LazyLoadingChildIndicator);
IsExpanded = false;
InLazyLoadingMode = true;
LazyLoadTriggered = false;
LazyLoadCompleted = false;
}
private string _caption;
public string Caption
{
get => _caption;
set => Set(nameof(Caption), ref _caption, value);
}
public void ClearChildren()
{
_childrenList.Clear();
}
/// <summary>
/// Add a new child item to this TreeView item
/// </summary>
/// <param name="child"></param>
public void AddChild(perTreeViewItemViewModelBase child)
{
if (LazyLoadChildrenOverridden)
{
throw new InvalidOperationException("Don't call AddChild for an item with LazyLoad mode set & LazyLoadChildren has been overridden");
}
if (_childrenList.Any() && _childrenList.First() == LazyLoadingChildIndicator)
{
_childrenList.Clear();
}
_childrenList.Add(child);
SetChildPropertiesFromParent(child);
}
protected void SetChildPropertiesFromParent(perTreeViewItemViewModelBase child)
{
child.Parent = this;
// if this node is checked then all new children added are set checked
if (IsChecked.GetValueOrDefault())
{
child.SetIsCheckedIncludingChildren(true);
}
ReCalculateNodeCheckState();
}
protected void ReCalculateNodeCheckState()
{
var item = this;
while (item != null)
{
if (item.Children.Any() && !Equals(item.Children.FirstOrDefault(), LazyLoadingChildIndicator))
{
var hasIndeterminateChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.HasValue);
if (hasIndeterminateChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
var hasSelectedChild = item.Children.Any(c => c.IsEnabled && c.IsChecked.GetValueOrDefault());
var hasUnselectedChild = item.Children.Any(c => c.IsEnabled && !c.IsChecked.GetValueOrDefault());
if (hasUnselectedChild && hasSelectedChild)
{
item.SetIsCheckedThisItemOnly(null);
}
else
{
item.SetIsCheckedThisItemOnly(hasSelectedChild);
}
}
}
item = item.Parent;
}
}
private void SetIsCheckedIncludingChildren(bool? value)
{
if (IsEnabled)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
}
}
private void SetIsCheckedThisItemOnly(bool? value)
{
_isChecked = value;
RaisePropertyChanged(nameof(IsChecked));
}
/// <summary>
/// Add multiple children to this TreeView item
/// </summary>
/// <param name="children"></param>
public void AddChildren(IEnumerable<perTreeViewItemViewModelBase> children)
{
foreach (var child in children)
{
AddChild(child);
}
}
/// <summary>
/// Remove a child item from this TreeView item
/// </summary>
public void RemoveChild(perTreeViewItemViewModelBase child)
{
_childrenList.Remove(child);
child.Parent = null;
ReCalculateNodeCheckState();
}
public perTreeViewItemViewModelBase Parent { get; private set; }
private bool? _isChecked = false;
public bool? IsChecked
{
get => _isChecked;
set
{
if (Set(nameof(IsChecked), ref _isChecked, value))
{
foreach (var child in Children)
{
if (child.IsEnabled)
{
child.SetIsCheckedIncludingChildren(value);
}
}
Parent?.ReCalculateNodeCheckState();
}
}
}
private bool _isExpanded;
public bool IsExpanded
{
get => _isExpanded;
set
{
if (Set(nameof(IsExpanded), ref _isExpanded, value) && value && RequiresLazyLoad)
{
TriggerLazyLoading();
}
}
}
private bool _isEnabled = true;
public bool IsEnabled
{
get => _isEnabled;
set => Set(nameof(IsEnabled), ref _isEnabled, value);
}
public void TriggerLazyLoading()
{
var unused = DoLazyLoadAsync();
}
private async Task DoLazyLoadAsync()
{
if (LazyLoadTriggered)
{
return;
}
LazyLoadTriggered = true;
var lazyChildrenResult = await LazyLoadFetchChildren()
.EvaluateFunctionAsync()
.ConfigureAwait(false);
LazyLoadCompleted = true;
if (lazyChildrenResult.IsCompletedOk)
{
var lazyChildren = lazyChildrenResult.Data;
foreach (var child in lazyChildren)
{
SetChildPropertiesFromParent(child);
}
// If LazyLoadChildren has been overridden then just refresh the check state (using the new children)
// and update the check state (in case any of the new children is already set as checked)
if (LazyLoadChildrenOverridden)
{
ReCalculateNodeCheckState();
}
else
{
AddChildren(lazyChildren); // otherwise add the new children to the base collection.
}
}
RefreshChildren();
}
/// <summary>
/// Get the children for this node, in Lazy-Loading Mode
/// </summary>
/// <returns></returns>
protected virtual Task<perTreeViewItemViewModelBase[]> LazyLoadFetchChildren()
{
return Task.FromResult(new perTreeViewItemViewModelBase[0]);
}
/// <summary>
/// Update the Children property
/// </summary>
public void RefreshChildren()
{
RaisePropertyChanged(nameof(Children));
}
/// <summary>
/// In LazyLoading Mode, the Children property can be set to something other than
/// the base _childrenList collection - e.g as the union of two internal collections
/// </summary>
public IEnumerable<perTreeViewItemViewModelBase> Children => LazyLoadCompleted
? LazyLoadChildren
: _childrenList;
/// <summary>
/// How are the children held when in lazy loading mode.
/// </summary>
/// <remarks>
/// Override this as required in descendent classes - e.g. if Children is formed from a union
/// of multiple internal child item collections (of different types) which are populated in LazyLoadFetchChildren()
/// </remarks>
protected virtual IEnumerable<perTreeViewItemViewModelBase> LazyLoadChildren => _childrenList;
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
// if unselecting we don't care about anything else other than simply updating the property
if (!value)
{
Set(nameof(IsSelected), ref _isSelected, false);
return;
}
// Build a priority queue of operations
//
// All operations relating to tree item expansion are added with priority = DispatcherPriority.ContextIdle, so that they are
// sorted before any operations relating to selection (which have priority = DispatcherPriority.ApplicationIdle).
// This ensures that the visual container for all items are created before any selection operation is carried out.
//
// First expand all ancestors of the selected item - those closest to the root first
//
// Expanding a node will scroll as many of its children as possible into view - see perTreeViewItemHelper, but these scrolling
// operations will be added to the queue after all of the parent expansions.
var ancestorsToExpand = new Stack<perTreeViewItemViewModelBase>();
var parent = Parent;
while (parent != null)
{
if (!parent.IsExpanded)
{
ancestorsToExpand.Push(parent);
}
parent = parent.Parent;
}
while (ancestorsToExpand.Any())
{
var parentToExpand = ancestorsToExpand.Pop();
perDispatcherHelper.AddToQueue(() => parentToExpand.IsExpanded = true, DispatcherPriority.ContextIdle);
}
// Set the item's selected state - use DispatcherPriority.ApplicationIdle so this operation is executed after all
// expansion operations, no matter when they were added to the queue.
//
// Selecting a node will also scroll it into view - see perTreeViewItemHelper
perDispatcherHelper.AddToQueue(() => Set(nameof(IsSelected), ref _isSelected, true), DispatcherPriority.ApplicationIdle);
// note that by rule, a TreeView can only have one selected item, but this is handled automatically by
// the control - we aren't required to manually unselect the previously selected item.
// execute all of the queued operations in descending DispatcherPriority order (expansion before selection)
var unused = perDispatcherHelper.ProcessQueueAsync();
}
}
public override string ToString()
{
return Caption;
}
/// <summary>
/// What's the total number of child nodes beneath this one
/// </summary>
public int ChildCount => Children.Count() + Children.Sum(c => c.ChildCount);
}
The IsExpanded link between data and UI controls that you require is then defined in a global style.
<Style
x:Key="perTreeViewItemContainerStyle"
TargetType="{x:Type TreeViewItem}">
<!-- Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem -->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
...
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="ItemContainerStyle" Value="{StaticResource perTreeViewItemContainerStyle}" />
</Style>
You can then build the data for the TreeView as a nested collection of perTreeViewItemViewModelBase descendent items, and set the IsExpanded property of each data item as required. Note that the key tenet of MVVM is separation of UI and data, so not a single mention of TreeViewItem anywhere other than in the style.
More details of this perTreeViewItemViewModelBase class and its usage on my blog post.
I have a simple form that as a dynamic set of Parent Labels ( could be 1 -10 labels ) that is updated on a timer, if all is good these labels stay green like:
If however one of the parents has a status change, i am trying to then display the offending child or childs and result in somewthing like this:
then once status returns to normal revert back to the original layout ( as above )
so currently i have a view like this :
<Grid>
<ItemsControl ItemsSource = "{Binding Path = CIs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Label Content = "{Binding Path = NodeName, Mode = OneWay}"
Background = "{Binding Path = NodeStatus, Mode = OneWay}"
Tag="{Binding Path = Nodeid, Mode = OneWay}"
Foreground="White"
FontFamily="Arial Black"
HorizontalContentAlignment="Center"
BorderBrush="Black"
BorderThickness="1,1,1,1"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
a viewmodel populating the view with a simple add:
if (Node.level == 3)
{
CIs.Add(new CI { NodeName = Node.name, NodeStatus = Node.status, Nodeid = Node.id });
}
and the basic model :
public class CIModel {}
public class CI : INotifyPropertyChanged {
private string nodeName;
private string nodeStatus;
private string nodeid;
public string NodeName {
get {
return nodeName;
}
set {
if (nodeName != value) {
nodeName = value;
RaisePropertyChanged("NodeName");
}
}
}
public string Nodeid
{
get
{
return nodeid;
}
set
{
if (nodeid != value)
{
nodeid = value;
RaisePropertyChanged("Nodeid");
}
}
}
public string NodeStatus
{
get
{
return nodeStatus;
}
set
{
if (nodeStatus != value)
{
nodeStatus = value;
RaisePropertyChanged("NodeStatus");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
would creating a child view somehow on the parent status change be a possible way? or can i create all the parent and childs and toggle there visibility when there status changes?
Thanks
Did you mean something like this?
// Put this in MainWindow()
public MainWindow()
{
InitializeComponent();
new Demo(this);
}
// Demo code
public class Demo
{
public Demo(FrameworkElement view)
{
View = view;
View.DataContext = this;
StartDemo();
}
private FrameworkElement View { get; }
public ObservableCollection<Parent> Parents { get; } = new ObservableCollection<Parent>();
public async void StartDemo()
{
var delay = 500;
foreach (var index in Enumerable.Range(0, 5))
{
var item = new Parent { Name = $"Parent {index + 1}" };
Parents.Add(item);
await Task.Delay(delay);
}
// Add errors
for (var i = 0; i < 3; i++)
{
Parents[1].Errors.Add(new Child { Name = $"Child {i + 1}" });
await Task.Delay(delay);
}
// Remove errors
while (Parents[1].Errors.Any())
{
Parents[1].Errors.RemoveAt(Parents[1].Errors.Count - 1);
await Task.Delay(delay);
}
// Remove parents
while (Parents.Any())
{
Parents.RemoveAt(Parents.Count-1);
await Task.Delay(delay);
}
}
}
/// <summary>
/// Child (error item)
/// </summary>
public class Child
{
public string Name { get; set; }
}
/// <summary>
/// Parent
/// </summary>
public class Parent : INotifyPropertyChanged
{
public Parent()
{
System.Collections.Specialized.CollectionChangedEventManager.AddHandler(Errors,
delegate
{
OnPropertyChanged(nameof(Status));
});
}
private string _name;
public string Name
{
get { return _name; }
set { _name = value; OnPropertyChanged(); }
}
public string Status { get { return Errors.Any() ? "ERROR" : "OK"; } }
/// <summary>
/// Children/errors
/// </summary>
public ObservableCollection<Child> Errors { get; } = new ObservableCollection<Child>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
<!-- Xaml -->
<ItemsControl ItemsSource="{Binding Path=Parents}"
Grid.IsSharedSizeScope="True">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:Parent}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="ParentColumn" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- Parent label -->
<Label Content="{Binding Path=Name}"
x:Name="Label"/>
<!-- Errors -->
<ItemsControl ItemsSource="{Binding Path=Errors}"
Grid.Column="1">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:Child}">
<Label Content="{Binding Path=Name}"
Background="Red" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
<DataTemplate.Triggers>
<!-- Parent is ok -->
<DataTrigger Binding="{Binding Path=Status}"
Value="OK">
<Setter TargetName="Label" Property="Background" Value="Green" />
</DataTrigger>
<!-- Parent is not ok -->
<DataTrigger Binding="{Binding Path=Status}"
Value="ERROR">
<Setter TargetName="Label" Property="Background" Value="Red" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I constructed a treeView WPF MVVM with the help of this very good article
Then I created a contextMenu for some node that allowed me to add children from selected parent.
The problem is if I click on "Add" without expanding manually the selected node(parent), a strange child is created automatically in addition to the node expected to be generated when clicking on "Add".
I tried to detect the problem so I change the code below from:
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
to:
<Setter Property="IsExpanded" Value="True" />
Image 1 below shows the result of this test or Image 2 shows what my treeView must show.
image1
image2
Rq: I used image from the article that I talked about it. Also, I used the same approach described in the article (including the class TreeViewItemViewModel.cs )
Base class for all ViewModel
public class TreeViewItemViewModel : INotifyPropertyChanged
{
#region Data
static readonly TreeViewItemViewModel DummyChild = new TreeViewItemViewModel();
readonly ObservableCollection<TreeViewItemViewModel> _children;
readonly TreeViewItemViewModel _parent;
bool _isExpanded;
bool _isSelected;
#endregion // Data
#region Constructors
protected TreeViewItemViewModel(TreeViewItemViewModel parent, bool lazyLoadChildren)
{
_parent = parent;
_children = new ObservableCollection<TreeViewItemViewModel>();
if (lazyLoadChildren)
_children.Add(DummyChild);
}
// This is used to create the DummyChild instance.
private TreeViewItemViewModel()
{
}
#endregion // Constructors
#region Presentation Members
#region Children
/// <summary>
/// Returns the logical child items of this object.
/// </summary>
public ObservableCollection<TreeViewItemViewModel> Children
{
get { return _children; }
}
#endregion // Children
#region HasLoadedChildren
/// <summary>
/// Returns true if this object's Children have not yet been populated.
/// </summary>
public bool HasDummyChild
{
get { return this.Children.Count == 1 && this.Children[0] == DummyChild; }
}
#endregion // HasLoadedChildren
#region IsExpanded
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is expanded.
/// </summary>
public bool IsExpanded
{
get { return _isExpanded; }
set
{
if (value != _isExpanded)
{
_isExpanded = value;
this.OnPropertyChanged("IsExpanded");
}
// Expand all the way up to the root.
if (_isExpanded && _parent != null)
_parent.IsExpanded = true;
// Lazy load the child items, if necessary.
if (this.HasDummyChild)
{
this.Children.Remove(DummyChild);
this.LoadChildren();
}
}
}
#endregion // IsExpanded
#region IsSelected
/// <summary>
/// Gets/sets whether the TreeViewItem
/// associated with this object is selected.
/// </summary>
public bool IsSelected
{
get { return _isSelected; }
set
{
if (value != _isSelected)
{
_isSelected = value;
this.OnPropertyChanged("IsSelected");
}
}
}
#endregion // IsSelected
#region LoadChildren
/// <summary>
/// Invoked when the child items need to be loaded on demand.
/// Subclasses can override this to populate the Children collection.
/// </summary>
protected virtual void LoadChildren()
{
}
#endregion // LoadChildren
#region Parent
public TreeViewItemViewModel Parent
{
get { return _parent; }
}
#endregion // Parent
#endregion // Presentation Members
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion // INotifyPropertyChanged Members
}
Myxml:
<TreeView ItemsSource="{Binding Regions}" IsEnabled="{Binding EnableTree}" >
<TreeView.ItemContainerStyle>
<!--
This Style binds a TreeViewItem to a TreeViewItemViewModel.
-->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<ContextMenu x:Key="AddCity" ItemsSource="{Binding AddCityItems}"/>
<HierarchicalDataTemplate
DataType="{x:Type local:StateViewModel}"
ItemsSource="{Binding Children}"
>
<StackPanel Orientation="Horizontal" ContextMenu="{StaticResource AddCity}">
<Image Width="16" Height="16" Margin="3,0" Source="Images\Region.png" />
<TextBlock Text="{Binding RegionName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
RegionViewModel:
`public class StateViewModel : TreeViewItemViewModel
{
readonly State _state;
public ICommand AddCityCommand { get; private set; }
public List<MenuItem> AddCityItems { get; set; }
public StateViewModel(State state, RegionViewModel parentRegion)
: base(parentRegion, true)
{
_state = state;
AddCityItems = new List<MenuItem>();
AddCityCommand = new DelegateCommand<CancelEventArgs>(OnAddCityCommandExecute, OnAddCityCommandCanExecute);
AddCityItems.Add(new MenuItem() { Header = "Add City", Command = AddCityCommand });
}
public string StateName
{
get { return _state.StateName; }
}
protected override void LoadChildren()
{
foreach (City city in Database.GetCities(_state))
base.Children.Add(new CityViewModel(city, this));
}
bool OnAddCityCommandCanExecute(CancelEventArgs parameter)
{
return true;
}
public void OnAddCityCommandExecute(CancelEventArgs parameter)
{
var myNewCity = new city();
Children.Add(new CityViewModel(myNewCity, this));
}
}`
BTW, if I expand my parent node then I click into add City, I have the result as expected but if I don't expand parent node and I click on contextMenu I have another child created in addition to the child I want to create
EDIT
I add the statemnt below to my add() method and I don't have any problem now:
public void OnAddCityCommandExecute(CancelEventArgs parameter)
{
var myNewCity = new city();
Children.Add(new CityViewModel(myNewCity, this));
//the modif
this.Children.Remove(DummyChild);
}
I can see the bug in your code.
Here's the steps to reproduce:
At state node (never expand it first)
Without Expanding the Child upfront, Your StateViewModel's Children contain a DummyChild.
Added 1 new City into the list which cause the HasDummyChild won't work as the count is now 2 in Children's list
Then when you try to expand the node to check the result. Your treelist will have the DummyChild which is a base class that screwed up everything
So, basically that's why "Expand" first is the key of your problem as at that time HasDummyChild still working as it compares .Count == 1. The tree won't remove the DummyChild out from your Children list if you add an extra child to the list that makes .Count == 2.
ADDITIONAL INFO as requested
Just change the HasDummyChild as the following
public bool HasDummyChild
{
//get { return this.Children.Count == 1 && this.Children[0] == DummyChild; }
get { return Children.Any() && Children.Contains(DummyChild); }
}