I have a .NET 5 project with following Nuget Packages:
HandyControls for UI
Gong-Wpf-DragDrop for Drag and Drop elements in a List
I have a XAML with a ListBoxand a ViewModel with aObservableCollection` of Model.
The ObservableCollection is binded as ItemSource of ListBox
What I want to achieve:
When i Drag and Drop an item in a different position (or Add/Delete), I want the indexes to be refreshed.
Example:
Before Drag/Drop
After Drag/Drop
Actually, i binded the drophandler of gong-wpf-dragdrop
and at the end of the drop, i manually refresh every single Index in my list.
there is a way to do it easily? because actually i have to refresh indexes manually.
Summarizing:
When i reorder/delete/add items i want Model.Index of every item updated with the correct index position in ListBox.
My Mandate is:
Show index (one based)
Give the possibility to reorder the elements
I tried looking for similar questions but didn't find much that could help me.
Thanks in advance :)
Model:
public class Model : BindablePropertyBase
{
private int index;
private string name;
public int Index
{
get { return index; }
set
{
index = value;
RaisePropertyChanged();
}
}
public string Name
{
get { return name; }
set
{
name = value;
RaisePropertyChanged();
}
}
}
And below a xaml with a simple binded list
MainWindow.xaml
<hc:Window x:Class="ListBoxIndexTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:hc="https://handyorg.github.io/handycontrol"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dd="clr-namespace:GongSolutions.Wpf.DragDrop;assembly=GongSolutions.Wpf.DragDrop"
xmlns:local="clr-namespace:ListBoxIndexTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="600">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding TestList}" dd:DragDrop.DropHandler="{Binding}"
dd:DragDrop.IsDropTarget="True" dd:DragDrop.IsDragSource="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid Margin="10"
Background="Aqua">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Index}" Margin="10,0"/>
<TextBlock Text="{Binding Name}"
Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</hc:Window>
MainWindowViewModel.cs
public class MainWindowViewModel : BindablePropertyBase, IDropTarget
{
private ObservableCollection<Model> test_list;
public ObservableCollection<Model> TestList
{
get
{
return test_list;
}
set
{
test_list = value;
RaisePropertyChanged();
}
}
// Constructor
public MainWindowViewModel()
{
TestList = new ObservableCollection<Model>()
{
new Model()
{
Index = 1,
Name = "FirstModel"
},
new Model()
{
Index = 2,
Name = "SecondModel"
},
new Model()
{
Index = 3,
Name = "ThirdModel"
}
};
}
public void DragOver(IDropInfo dropInfo)
{
Model sourceItem = dropInfo.Data as Model;
Model targetItem = dropInfo.TargetItem as Model;
if (sourceItem != null && targetItem != null)
{
dropInfo.DropTargetAdorner = DropTargetAdorners.Insert;
dropInfo.Effects = DragDropEffects.Move;
}
}
public void Drop(IDropInfo dropInfo)
{
Model sourceItem = dropInfo.Data as Model;
Model targetItem = dropInfo.TargetItem as Model;
if(sourceItem != null && targetItem != null)
{
int s_index = sourceItem.Index - 1;
int t_index = targetItem.Index - 1;
TestList.RemoveAt(s_index);
TestList.Insert(t_index, sourceItem);
RefreshAllIndexes();
}
}
private void RefreshAllIndexes()
{
for (int i = 0; i < TestList.Count; i++)
{
TestList[i].Index = i + 1;
}
}
}
I don't believe there is an out-of-the box way to bind to a container index in WPF. Your solution is actually easy to understand.
If you find yourself binding often to index, you could create your own attached property/value converter that internally climbs up the visual tree using these helpers until it finds the parent ItemsControland makes use of the IndexFromContainer method.
Here is some code to get you started with this method:
First a small helper function to climb up the visual tree looking for an item of generic type:
public static DependencyObject FindParentOfType<T>(DependencyObject child) where T : DependencyObject {
//We get the immediate parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) {
return null;
}
//check if the parent matches the type we're looking for
if (parentObject is T parent) {
return parent;
} else {
return FindParentOfType<T>(parentObject);
}
}
Then a value converter that takes a control as input value and returns its index in the first encountered ItemsControl:
public class ContainerToIndexConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
//Cast the passed value as an ItemsControl container
DependencyObject container = value as ContentPresenter;
if (container == null) {
container = value as ContentControl;
}
//Finds the parent ItemsControl by looking up the visual tree
var itemControls = (ItemsControl)FindParentOfType<ItemsControl>(container);
//Gets the index of the container from the parent ItemsControl
return itemControls.ItemContainerGenerator.IndexFromContainer(container);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
throw new NotImplementedException();
}
}
And this is how you would use it in XAML:
<ListBox.ItemTemplate>
<DataTemplate>
<!-- This will display the index of the list item. -->
<TextBlock Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentPresenter}, Converter={StaticResource ContainerToIndexConverter}}" />
</DataTemplate>
</ListBox.ItemTemplate>
Related
I generate UI elements at runtime by using a ItemsControl. The UI generates successfully but if I am unable to get any properties of the generated UI items, such as "Content" for a label, or SelectedItem for a ComboBox. I tried to get these properties by using this tutorial , and these answers but I always get a NullReferenceException.
The ItemsControl in XAML looks like this:
<ItemsControl Name="ListOfVideos">
<ItemsControl.Background>
<SolidColorBrush Color="Black" Opacity="0"/>
</ItemsControl.Background>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="0,0,0,10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="200"/>
</Grid.ColumnDefinitions>
<Image HorizontalAlignment="Left" Height="100" Width="175" x:Name="VideoThumbnailImage" Stretch="Fill" Source="{Binding VideoThumbnailURL}" Grid.Column="0"></Image>
<Label x:Name="VideoTitleLabel" Content="{Binding VideoTitleText}" Foreground="White" Grid.Column="1" VerticalAlignment="Top" FontSize="16" FontWeight="Bold"></Label>
<Label x:Name="VideoFileSizeLabel" Content="{Binding VideoTotalSizeText}" Foreground="White" FontSize="14" Grid.Column="1" Margin="0,0,0,35" VerticalAlignment="Bottom"></Label>
<Label x:Name="VideoProgressLabel" Content="{Binding VideoStatusText}" Foreground="White" FontSize="14" Grid.Column="1" VerticalAlignment="Bottom"></Label>
<ComboBox x:Name="VideoComboBox" SelectionChanged="VideoComboBox_SelectionChanged" Grid.Column="2" Width="147.731" Height="20" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,50" ItemsSource="{Binding VideoQualitiesList}"></ComboBox>
<Label Content="Video Quality" Foreground="White" FontSize="14" VerticalAlignment="Top" Grid.Column="2" HorizontalAlignment="Center"></Label>
<Label Content="Audio Quality" Foreground="White" FontSize="14" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,27" Grid.Column="2"></Label>
<Slider x:Name="VideoAudioSlider" Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Bottom" Width="147.731" Maximum="{Binding AudioCount}"></Slider>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This is how I generate the UI elements
public class VideoMetadataDisplay
{
public string VideoTitleText { get; set; }
public int AudioCount { get; set; }
public string VideoThumbnailURL { get; set; }
public string VideoStatusText { get; set; }
public string VideoTotalSizeText { get; set; }
public List<string> VideoQualitiesList { get; set; }
}
public partial class PlaylistPage : Page
{
private void GetPlaylistMetadata()
{
List<VideoMetadataDisplay> newList = new List<VideoMetadataDisplay>();
//populate the list
ListOfVideos.ItemsSource = newList;
}
}
And this is how I'm trying to get the properties of the UI elements
public class Utils
{
public childItem FindVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child != null && child is childItem)
{
return (childItem)child;
}
else
{
childItem childOfChild = FindVisualChild<childItem>(child);
if (childOfChild != null)
return childOfChild;
}
}
return null;
}
}
private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UIElement CurrentItem = (UIElement)ListOfVideos.ItemContainerGenerator.ContainerFromItem(ListOfVideos.Items.CurrentItem);
Utils utils = new Utils();
ContentPresenter CurrentContentPresenter = utils.FindVisualChild<ContentPresenter>(CurrentItem);
DataTemplate CurrentDataTemplate = CurrentContentPresenter.ContentTemplate;
Label VideoTitle = (Label)CurrentDataTemplate.FindName("VideoTitleLabel", CurrentContentPresenter);
string VideoTitleText = VideoTitle.Content.ToString();
MessageBox.Show(VideoTitleText);
}
Every time I try to run this, FindVisualChild always returns one of the labels (VideoTitleLabel) instead of returning the ContentPresenter for the currently active item. CurrentDataTemplate is then null and I am unable to get any of the UI elements from it.
It is impossible that FindVisualChild<ContentPresenter> returns a Label instance. FindVisualChild casts the result to ContentPresenter. Since Label is not a ContentPresenter, this would throw an InvalidCastException. But before this, child is childItem would return false in case child is of type Label and the generic parameter type childItem is of type ContentPresenter and therefore a potential null is returned.
Short Version
Accessing the the DataTemplate or looking up controls just to get their bound data is always too complicated. It's always easier to access the data source directly.
ItemsControl.SelectedItem will return the data model for the selected item. You are usually not interested in the containers.
private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
var item = listView.SelectedItem as VideoMetadataDisplay;
MessageBox.Show(item.VideoTitleText);
}
Your Version (FindVisualChild improved)
FindVisualChild is weakly implemented. It will fail and throw an exception if traversal encounters a child node without children i.e. the parameter obj is null. You have to check the parameter obj for null before invoking VisualTreeHelper.GetChildrenCount(obj), to avoid a null reference.
Also you don't need to search the element by accessing the template. You can look it up directly in the visual tree.
I have modified your FindVisualChild method to search elements by name. I also have turned it into an extension method for convenience:
Extension Method
public static class Utils
{
public static bool TryFindVisualChildByName<TChild>(
this DependencyObject parent,
string childElementName,
out TChild childElement,
bool isCaseSensitive = false)
where TChild : FrameworkElement
{
childElement = null;
// Popup.Child content is not part of the visual tree.
// To prevent traversal from breaking when parent is a Popup,
// we need to explicitly extract the content.
if (parent is Popup popup)
{
parent = popup.Child;
}
if (parent == null)
{
return false;
}
var stringComparison = isCaseSensitive
? StringComparison.Ordinal
: StringComparison.OrdinalIgnoreCase;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is TChild resultElement
&& resultElement.Name.Equals(childElementName, stringComparison))
{
childElement = resultElement;
return true;
}
if (child.TryFindVisualChildByName(childElementName, out childElement))
{
return true;
}
}
return false;
}
}
Example
private void VideoComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
object item = listView.SelectedItem;
var itemContainer = listView.ItemContainerGenerator.ContainerFromItem(item) as ListViewItem;
if (itemContainer.TryFindVisualChildByName("VideoTitleLabel", out Label label))
{
var videoTitleText = label.Content as string;
MessageBox.Show(videoTitleText);
}
}
I've got a hierarchical data structure which i'm trying to visualize with a WPF TreeView and hierarchical data templates. The number of items can reach millions, so i decided to try the virtualization features which work easy and well, besides that one problem which arises when i use "Recycling" instead of "Standard" for the VirtualizationMode. A long time ago someone else seemed to have a similar problem:
"BindingExpression path error" using ItemsControl and VirtualizingStackPanel
Nevertheless, i have troubles to implement a working and performant filtering.
I tried two different approaches, based on hints i could find on the internet, like
How to filter a TreeView in WPF?
In WPF can you filter a CollectionViewSource without code behind?
Filter WPF TreeView using MVVM
How to filter a wpf treeview hierarchy using an ICollectionView?
WPF: Filter TreeView without collapsing it's nodes
Filtering Treeview
The first is to use a Converter to set TreeViewItem.Visibility based on a filter, the second is to create ICollectionView ad hoc for all elements and assign the same filter predicate to each one.
I like the first approach because it requires less code, seems to be more clear, and requires less hacks, and i feel like i had to use that one hack (HackToForceVisibilityUpdate) only because i don't know better, but it slows down the UI considerably and i don't know how to fix that.
The problem with the second approach is that it collapses all nodes when the filter is changed (which i think could be fixed by tracking the states before the filter change and restoring them afterwards) and that it involves a lot of additional code (for the sake of the example, a singleton hack is used to not blow the code up too much and adding/removing items won't work).
I feel like both approaches could be fixed but i cannot figure out how and i really would like to fix the performance issues of the first approach.
It's a lot of code to show in a post and if you don't like the code blocks below, here are the files as pastebin
approach 1 - cs
approach 1 - xaml
approach 2 - cs
approach 2 - xaml
and as an archive containing a VS solution and
a project WpfVirtualizedTreeViewPerItemVisibility for the 1st and
a project WpfVirtualizedTreeViewPerItemVisibility3 for the 2nd approach
which can be found here
The first approach:
The data structures:
public class TvItemBase {}
public class TvItemType1 : TvItemBase
{
public string Name1 { get; set; }
public List<TvItemBase> Entries { get; } = new List<TvItemBase>();
}
public class TvItemType2 : TvItemBase
{
public string Name2 { get; set; }
public int i { get; set; }
}
The converter looks like this (it corresponds to the "filterFunc" in the 2nd approach)
public class TvItemType2VisibleConverter : IMultiValueConverter
{
public TvItemType2VisibleConverter() { }
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values.Length < 2)
return Visibility.Visible;
var tvItem = values[0] as TreeViewItem;
if (tvItem == null)
return Visibility.Visible;
var entry = tvItem.DataContext as TvItemType2;
if (entry == null)
return Visibility.Visible;
var model = values[1] as IFilterProvider;
if (model == null)
return Visibility.Visible;
if (!model.ShowA)
return Visibility.Collapsed;
else if (entry.i % 2 == 0)
return Visibility.Collapsed;
else
return Visibility.Visible;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new System.NotImplementedException(); }
}
and the window impl, which is also the view model, looks like this
public interface IFilterProvider
{
bool ShowA { get; }
}
public partial class MainWindow : Window, INotifyPropertyChanged, IFilterProvider
{
public event PropertyChangedEventHandler PropertyChanged;
void NotifyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }
bool ShowA_ = true;
public bool ShowA
{
get { return ShowA_; }
set { ShowA_ = value; NotifyChanged(nameof(ShowA)); NotifyChanged(nameof(HackToForceVisibilityUpdate)); }
}
public bool HackToForceVisibilityUpdate { get { return true; } }
void generateTestItems(TvItemType1 parent, int nof1, int nof2, int levels)
{
for (int i = 0; i < nof1; i++)
{
var i1 = new TvItemType1 { Name1 = string.Format("F_{0}.{1}.{2}.{3}", levels, i, nof1, nof2) };
parent.Entries.Add(i1);
if (levels > 0)
generateTestItems(i1, nof1, nof2, levels - 1);
}
for (int i = 0; i < nof2; i++)
parent.Entries.Add(new TvItemType2 { Name2 = string.Format("{0}.{1}.{2}.{3}", levels, nof1 + i, nof1, nof2), i = nof1 + i });
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
var i1 = new TvItemType1 { Name1 = "root" };
generateTestItems(i1, 10, 1000, 3);
tv.ItemsSource = new List<TvItemBase> { i1 };
}
}
Finally, here's the xaml:
<Window.Resources>
<local:TvItemType2VisibleConverter x:Key="TvItemType2VisibleConverter"/>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType1}" ItemsSource="{Binding Entries}">
<TextBlock Text="{Binding Name1}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="Visibility">
<Setter.Value>
<MultiBinding Converter="{StaticResource TvItemType2VisibleConverter}">
<Binding RelativeSource="{RelativeSource Self}" />
<!-- todo: how to specify the filter provider through a view model property (using Path and Source?) -->
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:IFilterProvider}" />
<!-- todo: how to enforce filter reevaluation without this hack -->
<Binding Path="HackToForceVisibilityUpdate" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:IFilterProvider}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="IsExpanded" Value="True" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType2}">
<TextBlock Text="{Binding Name2}" />
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<ToggleButton Content="A" IsChecked="{Binding ShowA, Mode=TwoWay}" Width="20" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TreeView x:Name="tv"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="True"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard" Margin="0,24,0,0" />
<!-- todo: when using VirtualizingStackPanel.VirtualizationMode="Recycling", a lot of
System.Windows.Data Error: 40 : BindingExpression path error: 'Entries' property not found on 'object' ''TvItemType2' (HashCode=...)'. BindingExpression:Path=Entries; DataItem='TvItemType2' (HashCode=...); target element is 'TreeViewItem' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
are flooding the output window.
See f.e. https://stackoverflow.com/questions/4950208/bindingexpression-path-error-using-itemscontrol-and-virtualizingstackpanel -->
</Grid>
Code for the second approach:
The data structures:
public class TvItemBase {}
public class TvItemType1 : TvItemBase
{
public string Name1 { get; set; }
public List<TvItemBase> Entries { get; } = new List<TvItemBase>();
public ICollectionView FilteredEntries
{
get
{
var dv = CollectionViewSource.GetDefaultView(Entries);
dv.Filter = MainWindow.singleton.filterFunc; // todo:hack
return dv;
}
}
}
public class TvItemType2 : TvItemBase
{
public string Name2 { get; set; }
public int i { get; set; }
}
and the window impl, which is also the view model, looks like this
public interface IFilterProvider
{
bool ShowA { get; }
}
public partial class MainWindow : Window, INotifyPropertyChanged, IFilterProvider
{
public event PropertyChangedEventHandler PropertyChanged;
void NotifyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }
bool ShowA_ = true;
public bool ShowA
{
get { return ShowA_; }
set
{
ShowA_ = value;
// todo:hack
// todo:why does this update the whole tree
(tv.ItemsSource as ICollectionView).Refresh();
NotifyChanged(nameof(ShowA));
}
}
void generateTestItems(TvItemType1 parent, int nof1, int nof2, int levels)
{
for (int i = 0; i < nof1; i++)
{
var i1 = new TvItemType1 { Name1 = string.Format("F_{0}.{1}.{2}.{3}", levels, i, nof1, nof2) };
parent.Entries.Add(i1);
if (levels > 0)
generateTestItems(i1, nof1, nof2, levels - 1);
}
for (int i = 0; i < nof2; i++)
parent.Entries.Add(new TvItemType2 { Name2 = string.Format("{0}.{1}.{2}.{3}", levels, nof1 + i, nof1, nof2), i = nof1 + i });
}
public bool filterFunc(object obj)
{
var entry = obj as TvItemType2;
if (entry == null)
return true;
var model = this;
if (!model.ShowA)
return false;
else if (entry.i % 2 == 0)
return false;
else
return true;
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
singleton = this; // todo:hack
var i1 = new TvItemType1 { Name1 = "root" };
generateTestItems(i1, 10, 1000, 3);
//generateTestItems(i1, 3, 10, 3);
var l = new List<TvItemBase> { i1 };
var dv = CollectionViewSource.GetDefaultView(l);
dv.Filter = filterFunc;
tv.ItemsSource = dv;
}
public static MainWindow singleton = null; // todo:[really big]hack
}
Finally, here's the xaml:
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType1}" ItemsSource="{Binding FilteredEntries}">
<TextBlock Text="{Binding Name1}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType2}">
<TextBlock Text="{Binding Name2}" />
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<ToggleButton Content="A" IsChecked="{Binding ShowA, Mode=TwoWay}" Width="20" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TreeView x:Name="tv"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="True"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard" Margin="0,24,0,0" />
</Grid>
I managed to access control in the datatemplate of a GridViewItem, the following code:
private void btnChangePhoneNumber_Click(object sender, RoutedEventArgs e)
{
GridCell.SelectedItem = GridCell.Items[3];
var container = GridCell.ContainerFromIndex(3);
var _children = AllChildren(container);
var _control = _children.First(c => c.Name == "PhoneNumber");
_control.text = "123456789";
}
public List<TextBlock> AllChildrenText(DependencyObject parent)
{
var _List = new List<TextBlock> { };
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++)
{
var _Child = VisualTreeHelper.GetChild(parent, i);
if (_Child is TextBlock)
{
_List.Add(_Child as TextBlock);
}
_List.AddRange(AllChildrenText(_Child));
}
return _List;
}
where the GridCell is a Gridview.
This work.. but..
If I implement GridView with less than 40 items it's all right.
Unlike if I implement gridView with 10000 items, the text change that happens with the method: btnChangePhoneNumber_Click, also happens in other items ... and I can not understand the reason since, in the btnChangePhoneNumber_Click method, only one item is chosen.
Thanks in advance. A greeting.
I have tested your code, but I could not reproduce your issue in my side. As far as I'm concerned, It is low performance to render 10000 items in your GridView. And using VisualTreeHelper will bring about worse performance. You could bind
the text of TextBlock in the datatemplate with mvvm ViewModel. You just need
to modify the view model and the text of TextBlock will be changed. For more please refer to Data binding in depth. And the following is segment code of ViewModel.
MainPageViewModel.cs
public class MainPageViewModel : ViewModelBase
{
private ObservableCollection<Phone> _items;
public ObservableCollection<Phone> Items
{
get
{
return _items;
}
set
{
_items = value;
OnPropertyChanged();
}
}
public MainPageViewModel()
{
var list = new ObservableCollection<Phone>();
for (var i = 0; i < 1000; i++)
{
list.Add(new Phone { PhoneNumber = "123456" });
}
_items = list;
}
}
MainPage.xaml
<Page.DataContext>
<local:MainPageViewModel x:Name="ViewModel"/>
</Page.DataContext>
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Button Click="btnChangePhoneNumber_Click" Content=" click me"/>
<GridView x:Name="GridCell" Height="400" ItemsSource="{Binding Items}" >
<GridView.ItemTemplate>
<DataTemplate x:DataType="local:Phone">
<TextBlock Text="{x:Bind PhoneNumber ,Mode=OneWay}"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</StackPanel>
I have upload the code sample to github. Please check!
I've got a dilemma here. So I have mutiple expanders stacked on top of one another. Inside each expander is a ListBox, data bound, where each listitem displays a name of an object.
I've bound the search to filter the list items based on their name. However since I have two observable objects, the filtered items and unfiltered, the UI doesn't appear to get populated until someone searches. Whats the best way to fix this. I find it redundant to add items to both lists each time a new Person gets created. Using an mvvm approach.
The two collections are called People and PeopleFiltered. When i create people I add them to the list called People. When the search is applied it populates the PeopleFiltered list, which is the list the UI is bound to. How can I maintain this list be init to mimic People.
At the end of the day the PeopleFiltered collection should mimic People unless a search is being applied.
MainWindow.xaml
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
WindowStartupLocation="CenterScreen"
Title="MainWindow" Height="400" Width="200">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="Search:"/>
<TextBox Grid.Column="1" Background="Gold" Text="{Binding SearchString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<StackPanel Grid.Row="1">
<Expander Header="People" IsExpanded="{Binding IsExpanded, Mode=OneWay}">
<ListView ItemsSource="{Binding PeopleFiltered}">
<ListView.ItemTemplate>
<DataTemplate>
<WrapPanel>
<Ellipse Width="8" Height="8" Fill="Green" Margin="0,0,5,0"/>
<TextBlock Text="{Binding Name}"/>
</WrapPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</StackPanel>
</Grid>
</Window>
MainWindowViewModel.cs
using System.Collections.ObjectModel;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Collections.Generic;
namespace WpfApplication1
{
public class MainWindowViewModel : NotifyBase
{
// search text
private string searchString;
public string SearchString
{
get { return searchString; }
set
{
this.searchString = value;
NotifyPropertyChanged("SearchString");
ApplySearchFilter();
}
}
private void ApplySearchFilter()
{
if (string.IsNullOrWhiteSpace(SearchString))
{
IsExpanded = false;
PeopleFiltered.Clear();
foreach (DisplayItem displayItem in People)
{
PeopleFiltered.Add(displayItem);
}
}
else
{
// open expanders and apply search
IsExpanded = true;
PeopleFiltered.Clear();
foreach (DisplayItem displayItem in People)
{
if (displayItem.Name.ToLowerInvariant().Contains(SearchString.ToLowerInvariant()))
{
PeopleFiltered.Add(displayItem);
}
}
}
}
// used to to open and close expanders
private bool isExpanded;
public bool IsExpanded
{
get { return this.isExpanded; }
set
{
this.isExpanded = value;
NotifyPropertyChanged("IsExpanded");
}
}
// data collections for each expander
private ObservableCollection<DisplayItem> people;
public ObservableCollection<DisplayItem> People
{
get { return people ?? (people = new ObservableCollection<DisplayItem>()); }
set
{
people = value;
NotifyPropertyChanged("People");
}
}
private ObservableCollection<DisplayItem> peopleFiltered;
public ObservableCollection<DisplayItem> PeopleFiltered
{
get { return peopleFiltered ?? (peopleFiltered = new ObservableCollection<DisplayItem>()); }
set
{
peopleFiltered = value;
NotifyPropertyChanged("PeopleFiltered");
}
}
// init
public MainWindowViewModel()
{
// People
People.Add(new DisplayItem() { Name="John" });
People.Add(new DisplayItem() { Name="Veta"});
People.Add(new DisplayItem() { Name="Sammy"});
People.Add(new DisplayItem() { Name = "Sarah" });
People.Add(new DisplayItem() { Name = "Leslie" });
People.Add(new DisplayItem() { Name = "Mike" });
People.Add(new DisplayItem() { Name = "Sherry" });
People.Add(new DisplayItem() { Name = "Brittany" });
People.Add(new DisplayItem() { Name = "Kevin" });
}
}
// class used to display all items
public class DisplayItem
{
public string Name { get; set; }
}
//observable object class
public class NotifyBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
Here's a nice approach.
Create a Query property in your View Model, this will be bound to your filter TextBox.
private string _Query;
public string Query
{
get { return _Query; }
set
{
_Query = value;
Filter();
//Notify property changed.
}
}
One thing to note here is the Filter() method. This will be called every time the property changes, I'll get back to this later. Firstly, make sure your TextBox binding is TwoWay, it'll look like this:
<TextBox Text="{Binding Query}" ... />
In your View Model, you will need a collection for each ListBox.
private List<object> _Collection1; //The original collection
private List<object> _FilteredCollection1; //The filtered collection
public List<object> FilteredCollection1
{
get { return _FilteredCollection1; }
set
{
_FilteredCollection1 = value;
//Notify property changed.
}
}
//Some more collections
...
It's important to note here that there is a variable for the original unfiltered collection. This is important because we want to filter this list into a new collection, otherwise we'll just keep filtering over and over and eventually have nothing in the collection.
You'll need to bind the ItemsSource property in your ListBox to the collection(s).
<ListBox ItemsSource="{Binding FilteredCollection1}" ... />
Now, your Filter method can simply filter the _Collection1 variable into the FilteredCollection1 property.
private void Filter()
{
//Perform the filter here for all collections.
FilteredCollection1 = _Collection1.Where(x => x.Something == Query);
//Do the same for all other collections...
}
Note: The above Linq is just an example, I expect yours will be slightly more complicated than that, but you get the idea.
So. Whenever Query gets updated, the Filter collection will fire, update the FilteredCollection properties which will in turn call property changed and update the view.
Alternative Approach
Here's another way.
Instead of using a Filter method, you can instead put your filter code inside the get block in the FilteredCollection properties, like this:
public List<object> FilteredCollection1
{
get
{
return _Collection1.Where(...);
}
}
Then, in your Query property, simply call INotifyPropertyChanged for the collection:
private string _Query;
public string Query
{
get { return _Query; }
set
{
_Query = value;
//Notify property changed.
OnPropertyChanged("FilteredCollection1");
}
}
This will force the view to refresh the FilteredCollection1 property.
Some function provides a flat list of strings, which looks like:
var list = new List<int>{1,2,3,4};
A TreeView now wants to transform this list into a tree with two root nodes (one for odd numbers and one for even numbers). This is just an example as the real scenario must create a much more hierachical structure.
The point is that the backend provides a flat list and the view wants to transform it into a tree.
We tried a Converter as the ItemsSource but as it creates a new structure it basically breaks the binding to the original list (makes async filling impossible). Explaining why
Here is a small reproduction code:
Codebehind:
public partial class MainWindow : Window
{
public ObservableCollection<int> TreeViewSource { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
TreeViewSource = new ObservableCollection<int>();
Action filler =() => { Enumerable.Range(0, 100).ToList().ForEach((i) => { Thread.Sleep(20); TreeViewSource.Add(i); }); };
Task.Run(() => filler()); // ASYNC CALL DOES NOT WORK
//filler(); // SYNC CALL DOES WORK
}
}
public class TreeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var sourceCollection = value as ObservableCollection<int>;
var outputCollection = new List<Item>();
var odd = new Item { Text = "not divisible by 2" };
var even = new Item { Text = "divisible by 2" };
even.Children.AddRange(sourceCollection.Where(x => x % 2 == 0).Select(x => new Item { Text = x.ToString() }));
odd.Children.AddRange(sourceCollection.Where(x => x % 2 != 0).Select(x => new Item { Text = x.ToString() }));
outputCollection.Add(odd);
outputCollection.Add(even);
return outputCollection;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class Item
{
public string Text { get; set; }
public List<Item> Children { get; set; }
public Item()
{
Children = new List<Item>();
}
}
Xaml:
<Window x:Class="TreeViewAsync.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TreeViewAsync"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:TreeConverter x:Key="treeConverter"/>
</Window.Resources>
<Grid>
<TreeView Name="treeView"
ItemsSource="{Binding TreeViewSource, Converter={StaticResource treeConverter}}">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Item}" ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
This link says that a DataTemplate should be used instead of a Converter on an ItemsSource. But since the template is applied to each item individually, how can it create a full tree?
Again: The ViewModel does not need the data to be a tree and therefore does not provide a treestructure but a flat list only. The View wants to display it as a tree for convenience.
Due to MVVM we would like to avoid codebehind in the View.
A TreeView now wants to transform this list into a tree with two root nodes
The ViewModel does not need the data to be a tree and therefore does not provide a treestructure but a flat list only
You're misunderstanding View Model concept. If the View needs hierarchical data, then the View Model responsibility is to make data hierarchical and ready to be bound with view.
But since the template is applied to each item individually
Usually, templates are applied to any type of items.
public sealed class NestedItemsViewModel
{
public string Name { get; set; }
public int[] Items { get; set; }
}
public sealed class ViewModel
{
private readonly List<int> list = new List<int> { 1, 2, 3, 4 };
private NestedItemsViewModel[] items;
public NestedItemsViewModel[] Items
{
get
{
if (items == null)
{
items = new[]
{
new NestedItemsViewModel
{
Name = "Even",
Items = list.Where(x => x % 2 == 0).ToArray()
},
new NestedItemsViewModel
{
Name = "Odd",
Items = list.Where(x => x % 2 != 0).ToArray()
},
};
}
return items;
}
}
}
XAML:
<!-- Assuming, that TreeView.DataContext = new ViewModel() -->
<TreeView ItemsSource="{Binding Items, IsAsync=True}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:NestedItemsViewModel}" ItemsSource="{Binding Items}">
<TextBlock Text="{Binding Name}"/>
</HierarchicalDataTemplate>
<!-- If nested items will be more complex, then here will be their data template(s) -->
</TreeView.Resources>
</TreeView>