I've been checking out the UI virtualization feature for WPF's TreeView control, which as I understand, is available since .NET 3.5 SP1.
I made a simple project to make sure that UI virtualization is performed correctly, and found out that it doesn't work at all - all of the items are retrieved rather than just the ones currently displayed on the screen.
My XAML looks like this
<TreeView x:Name="myTree" Height="150" ItemsSource="{Binding Items}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard"
ScrollViewer.IsDeferredScrollingEnabled="True" />
And my code behind
public IEnumerable Items { get; set; }
public MainWindow()
{
Items = GenerateList();
this.DataContext = this;
InitializeComponent();
}
private IEnumerable GenerateList()
{
MyList list = new MyList();
for (int i = 0; i < 1000; i++)
{
list.Add("Item " + i);
}
return list;
}
Note that MyList is my own implementation of IList that holds an ArrayList and does nothing more than forward calls to the held ArrayList and write to the console which method/property was called. For example:
public object this[int index]
{
get
{
Debug.WriteLine(string.Format("get[{0}]", index));
return _list[index];
}
set
{
Debug.WriteLine(string.Format("set[{0}]", index));
_list[index] = value;
}
}
If I replace my TreeView with a ListBox, UI virtualization works as expected - i.e. only ~20 items are requested and not the whole 1000.
Am I doing something wrong here?
EDIT
I've also tried replacing the default ItemsPanel to VirtualizingStackPanel, as suggested , but I'm getting the same results.
Default ItemsPanelTemplate for TreeView is StackPanel and not VirtualizingStackPanel that's why you can't see virtualization in it. Whereas for ListBox default ItemsPanelTemplate is VirtualizingStackPanel that's why setting VirtualizingStackPanel.IsVirtualizing="True" works for ListBox.
To enable virtualization on your TreeView apart from setting property VirtualizingStackPanel.IsVirtualizing="True", you need to override the its default itemsPanelTemplate like this -
<TreeView x:Name="myTree" Height="150" ItemsSource="{Binding Items}"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard"
VirtualizingStackPanel.CleanUpVirtualizedItem="myTree_CleanUpVirtualizedItem"
ScrollViewer.IsDeferredScrollingEnabled="True">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
</TreeView>
Related
I have a ListView that is intended to show every product within a database, and it works for the most part, but when I scroll down by dragging the scroll bar, the bottom items end up being incorrect.
XAML Definition:
<ListView x:Name="lst_Products" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="16,124,16,16" Width="300" ContainerContentChanging="lst_Products_ContainerContentChanging" Loaded="lst_Products_Loaded" BorderBrush="Black" BorderThickness="2" CornerRadius="16">
<ListView.ItemTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Value}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
The data template is present so I can easily grab a product ID number with SelectedValue. According to some trusted community member (or whatever they call the prominent posters) on the MSDN forums said that's the only way to properly show a ListView when the ItemsSource is an ObservableCollection<KeyValuePair<int,RelativePanel>> while having a selectable value member.
The relevant C# code:
private async void lst_Products_Loaded(object sender, RoutedEventArgs e)
{
var products = await ProductManager.GetProducts();
ObservableCollection<KeyValuePair<int, RelativePanel>> productList = new(products);
lst_Products.ItemsSource = productList;
lst_Products.SelectedValuePath = "Key";
}
private void lst_Products_ContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
if (args.ItemIndex % 2 == 1)
{
args.ItemContainer.Background = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128));
}
else
{
args.ItemContainer.Background = UIManager.GetDefaultBackground();
}
}
public static async Task<List<KeyValuePair<int, RelativePanel>>> GetProducts()
{
var productPanels = new List<KeyValuePair<int, RelativePanel>>();
var productIDs = await SqlHandler.ReturnListQuery<int>($"SELECT id FROM {productTable}");
var productNames = await SqlHandler.ReturnListQuery<string>($"SELECT name FROM {productTable}");
var panels = new List<RelativePanel>();
foreach(var name in productNames)
{
RelativePanel panel = new();
TextBlock productName = new()
{
Text = name
};
panel.Children.Add(productName);
panels.Add(panel);
}
for(int i = 0; i < productIDs.Count; i++)
{
productPanels.Add(new KeyValuePair<int, string>(productIDs[i], panels[i]));
}
return productPanels;
}
The call to SQL Handler just runs an SQL query and returns a list of the results. I can post the code if you need, but I can assure you there's no sorting going on.
A screenshot of what the list looks like. The bottom item should be "Coffee" - Button Test Product 2 is the second item in the list.
A screenshot of the SQL datatable with the "Coffee" product at the bottom where it should be.
In this case it's just the bottom item that's incorrect, however other times it has jumbled 5 or 6 entries near the bottom. This only seems to occur with the DataTemplate/ContentPresenter, but without that, the RelativePanel does not display correctly in the list. Eventually the list will show more information about the product and as far as I can tell, there's no good way to do that without converting the SQL data into a RelativePanel on the c# side.
I'm open to suggestions on solving either the jumbling problem with the template, or adjusting the xaml so that I don't need the template to display bulk sql data without needing the template but I'm at a loss.
c# - UWP ListView displays incorrect items upon rapid scrolling when it has a DataTemplate
The problem should be caused by listview virtualization, There are two ways to sloved this prolbem, one is disalbe listview virtualization by setting ItemsPanel as StackPanel like the following
<ListView>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
And the other way is implement INotifyCollectionChanged interface for your model class. for more please refer to Data binding in depth
It's not good practice that useRelativePanel collection as datasoure, the better way is make RelativePanel in your DataTemplate and bind with mode class property.
For example
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Index}" />
<TextBlock Text="{Binding IsItem}" />
<Image Source="{Binding ImageSource}" Visibility="Collapsed" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
I am using a GridView with a template selector and passing the item source in the code behind. The problem with this is that the VariableSizedWrapGrid is really slow, the collection passed being about 80 items big (collection is composed of a few strings). Removing the variableSizedWrapGrid solves the issue but leaves me with large gaps between the templates having smaller width.
here is the GridView:
<SemanticZoom x:Name="Zoom" Grid.Row="1" IsZoomedInViewActive="False" ViewChangeStarted="Zoom_ViewChangeStarted_1" IsZoomOutButtonEnabled="False" Margin="0,0,0,29" Grid.RowSpan="2">
<SemanticZoom.ZoomedInView>
<!-- Horizontal scrolling grid used in most view states -->
<GridView
x:Name="itemGridView"
AutomationProperties.AutomationId="ItemsGridView"
AutomationProperties.Name="Items"
TabIndex="1"
ItemTemplateSelector="{StaticResource DayTemplateSelector}"
SelectionMode="None"
IsSwipeEnabled="True"
ItemContainerStyle="{StaticResource GridViewItemStyle1}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
IsItemClickEnabled="True"
ScrollViewer.IsHorizontalScrollChainingEnabled="False"
ItemClick="ItemView_ItemClick">
<GridView.ItemsPanel>
<ItemsPanelTemplate>
<VariableSizedWrapGrid ItemWidth="380" ItemHeight="500"/>
</ItemsPanelTemplate>
</GridView.ItemsPanel>
</GridView>
</SemanticZoom.ZoomedInView>
And the Template Selector:
class DayTemplateSelecter : DataTemplateSelector
{
public DataTemplate DayOffTemplate { get; set; }
public DataTemplate DutyTemplate { get; set; }
public DataTemplate RestTemplate { get; set; }
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
var templateItem = item as DutyItem;
var element = container as FrameworkElement;
if (templateItem.Trip.Count > 0 || templateItem.OtherTrip.Count > 0)
{
container.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, 2);
return DutyTemplate;
}
else if (templateItem.Codes.Count > 0)
{
container.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, 1);
return DayOffTemplate;
}
else
{
container.SetValue(VariableSizedWrapGrid.ColumnSpanProperty, 1);
return RestTemplate;
}
}
}
I didn't think this would have such a large performance difference..just using variableSized would give the page 3 seconds of delay and this is even larger on some low end tablets.
Am I doing it wrong, is there a better way to do this?
VariableSizedWrapGrid unlike WrapGrid doesn't virtualize its items. I would suggest that if you have to show 80 items use WrapGrid or if you have to use VariableSizedWrapGrid reduce amount of items to a more manageable level.
I've made a nice little three-item wide list of tiles that work as switches. It looks something like this:
Looking good huh? Well, I have about 130 of these tiles in a vertically scrolling list, and it takes ages to load. According to the performance analysis tool, each element takes about 18ms to render - which gives me about a 2.3 second rendering time. On the device, it's often twice that time. This wouldn't really be a crisis, but the UI is totally black and unresponsive up until these elements have been drawn.
After some research online, I realized this is because the WrapPanel control from the toolkit doesn't virtualize its items - thus making the GPU render all objects at once (using up a lot of memory in the process).
Now, are there any ways to make this go faster?
XAML:
<ListBox x:Name="ChannelsListBox" Grid.Row="2" Margin="0,40,0,0">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<toolkit:WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Template>
<ControlTemplate>
<ItemsPresenter />
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid x:Name="ChannelTile" Margin="6,6,6,6" Tap="ChannelTile_Tap">
<!-- context menu code removed -->
<Rectangle Width="136" Height="136" Fill="{StaticResource LightGrayColor}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
The ListBox's ItemsSource is set in the codebehind - if you wondered.
Well, if you populate the listbox asynchronously from another thread, you can avoid the unresponsive UI.
EDITED2:
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
/* In the xaml code:
<ListBox x:Name="ChannelsListBox" ItemsSource="{Binding ListOfTestClasses}" ...
*/
var vm = new MainPageViewModel();
DataContext = vm;
vm.StartLoadingDataAsync(10000);
}
}
public class MainPageViewModel
{
public ObservableCollection<TestClass> ListOfTestClasses { get; set; }
private BackgroundWorker workerThread;
public MainPageViewModel()
{
ListOfTestClasses = new ObservableCollection<TestClass>();
workerThread = new BackgroundWorker();
workerThread.DoWork += new DoWorkEventHandler((object sender, DoWorkEventArgs e) =>
{
for (int i = 0; i < (int)e.Argument; i++)
{
Deployment.Current.Dispatcher.BeginInvoke(() =>
{
ListOfTestClasses.Add(new TestClass { Text = "Element " + (i + 1) });
});
Thread.Sleep(150);
}
});
}
public void StartLoadingDataAsync(int numberOfElements)
{
workerThread.RunWorkerAsync(numberOfElements);
}
}
public class TestClass
{
public string Text { get; set; }
}
A few ideas which might be helpful:
Find or implement a virtualizing WrapPanel. It's the most appropriate solution, but I don't think I've seen a solid implementation of one yet. But for this purpose, maybe you don't need perfection and can get away with something someone else has already written.
Use a parent virtualizing vertical StackPanel containing horizontal StackPanel children. To do this, you'd need to re-shape your single sequence of data into a shorter sequence of 3-item entries. However, that may not be too hard and should give you most of the benefits of the ideal solution.
Consider implementing "lazy" containers like I did for DeferredLoadListBox. The basic idea is to delay rendering containers until they show up on screen. I have more info and example code here: http://blogs.msdn.com/b/delay/archive/2010/09/08/never-do-today-what-you-can-put-off-till-tomorrow-deferredloadlistbox-and-stackpanel-help-windows-phone-7-lists-scroll-smoothly-and-consistently.aspx
I am quite new to Windows development and of course even newer to Metro style app development. I am not sure I understand how Data Binding works.
I have a list of items.
private List<Expense> _expenses = new List<Expense>();
public List<Expense> Items
{
get
{
return this._expenses;
}
}
Which I bind to the XAML. (I use the Split Page template)
protected override void OnNavigatedTo(NavigationEventArgs e)
{
this.DefaultViewModel["Items"] = _data.Items;
}
Then I display it
<UserControl.Resources>
<CollectionViewSource
x:Name="itemsViewSource"
Source="{Binding Items, Mode=TwoWay}"/>
</UserControl.Resources>
<ListView
x:Name="itemListView"
AutomationProperties.AutomationId="ItemsListView"
AutomationProperties.Name="Items"
Margin="120,0,0,60"
ItemsSource="{Binding Source={StaticResource itemsViewSource}}"
SelectionChanged="ItemListView_SelectionChanged"
ItemTemplate="{StaticResource DefaultListItemTemplate}"/>
Which works fine. Then when the user clicks on a Button I add a new item to my list
_data.Items.Add(new Expense
{
Total = 100,
When = new DateTime(2013, 6, 6),
For = "Myself"
});
I was expecting that the ListView would refresh automagically since I set Mode=TwoWay but it does not. Did I misunderstand the concept and it is not possible for the list to refresh? Otherwise, what could I have done wrong?
In order to have the UI update after you make changes to the collection you need it to implement INotifyCollectionChanged. This will notify the UI when a change occurs and it will respond by rebinding the UI on top of the change.
Implementing this interface is fairly involved though. Instead you should just use ObservableCollection<T> in place of List<T> and the scenario should work just fine
private ObservableCollection<Expense> _expenses = new ObservableCollection<Expense>();
public ObservableCollection<Expense> Items
{
get
{
return this._expenses;
}
}
Scenario
I have a TreeView that is bound to ObservableCollection<T>. The collection gets modified every time the end-user modifies their filters. When users modify their filters a call to the database is made (takes 1-2ms tops) and the data returned gets parsed to create a hierarchy. I also have some XAML that ensures each TreeViewItem is expanded, which appears to be part of the problem. Keep in mind that I'm only modifying ~200 objects with a max node depth of 3. I would expect this to instant.
Problem
The problem is that whenever filters get modified and the TreeView hierarchy gets changed the UI hangs for ~1 second.
Here is the XAML responsible for create the TreeView hierarchy.
<TreeView VerticalAlignment="Top" ItemsSource="{Binding Hierarchy}" Width="240"
ScrollViewer.VerticalScrollBarVisibility="Auto" Grid.Row="1">
<TreeView.ItemTemplate>
<!-- Hierarchy template -->
<HierarchicalDataTemplate ItemsSource="{Binding Stations}">
<TextBlock Text="{Binding}" />
<!-- Station template -->
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Locates}">
<TextBlock Text="{Binding Name}" />
<!-- Locate template -->
<HierarchicalDataTemplate.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TicketNo}" />
</DataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</HierarchicalDataTemplate.ItemTemplate>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
And here is the code for updating the list.
public ObservableCollection<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
set { _hierarchy = value; }
}
public void UpdateLocates(IList<string> filterIds)
{
_hierarchy.Clear();
// Returns 200 records max
var locates = _locateRepository.GetLocatesWithFilters(filterIds);
var dates = locates.Select(x => x.DueDate);
foreach (var date in dates)
{
var vm = new HierarchyViewModel
{
DueDate = date
};
var groups = locates.Where(x => x.DueDate.Date.Equals(date.Date)).GroupBy(x => x.SentTo);
// Logic ommited for brevity
_hierarchy.Add(vm);
}
}
I also have <Setter Property="IsExpanded" Value="True" /> as a style. I have tried using a BindingList<T> and disabling notifications, but that didn't help.
Any ideas as to why my UI hangs whenever changes are made to the ObservableCollection<T>?
Partial Solution
With what H.B. said and implementing a BackgroundWorker the update is much more fluid.
The problem is probably the foreach loop. Every time you add an object the CollectionChanged event is fired and the tree is rebuilt.
You do not want to use an ObservableCollection if all you do is clear the whole list and replace it with a new one, use a List and fire a PropertyChanged event once the data is fully loaded.
i.e. just bind to a property like this (requires implementation of INotifyPropertyChanged):
private IEnumerable<HierarchyViewModel> _hierarchy = null;
public IEnumerable<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
set
{
if (_hierarchy != value)
{
_hierarchy = value;
NotifyPropertyChanged("Hierarchy");
}
}
}
If you set this property bindings will be notified. Here i use the IEnumerable interface so no-one tries to just add items to it (which would not be noticed by the binding). But this is just one suggestion which may or may not work for your specific scenario.
(Also see sixlettervariable's good comment)
Just a side note, this code:
public ObservableCollection<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
set { _hierarchy = value; }
}
is bad, you could overwrite the list and the binding would break because there is no PropertyChanged event being fired in the setter.
If you use an ObservableCollection it normally is used like this:
private readonly ObservableCollection<HierarchyViewModel> _hierarchy =
new ObservableCollection<HierarchyViewModel>();
public ObservableCollection<HierarchyViewModel> Hierarchy
{
get { return _hierarchy; }
}
The easiest thing to do is detach the item you are bound to, make all the changes you need to the list, then reattach it.
For example, set the treeviews ItemsSource to NULL/NOTHING, run through your for each, then set the ItemsSource back to _hierarchy. Your adds will be instant.