I have a WPF application that searches through a large data set and displays the results in a ListView. The search can return a small result set, or the result set can be thousands of items. Searching the data set and returning the result set takes less than a second. The return set is an ObservableCollection. My ListView is slow when it's being loaded. The ListView is bound to the ObservableCollection in the XAML. This is the XAML:
<GroupBox Header="Translations" Grid.Row="2" Margin="10,0,10,8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="35" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ListView Grid.ColumnSpan="2"
ItemsSource="{Binding FoundItems}"
SelectionMode="Single"
MaxHeight="2000"
VirtualizingPanel.IsContainerVirtualizable="True"
VirtualizingPanel.IsVirtualizing="True">
<ListView.View>
<GridView>
<GridViewColumn Header="Translation File Name" Width="NaN" DisplayMemberBinding="{Binding Path=FileName}" />
<GridViewColumn Header="English" Width="400" DisplayMemberBinding="{Binding Path=English}" />
<GridViewColumn Header="International" Width="400" DisplayMemberBinding="{Binding Path=International}" />
</GridView>
</ListView.View>
</ListView>
<Border Grid.Row="1" Grid.ColumnSpan="2" BorderThickness="1" BorderBrush="{DynamicResource AccentColorBrush}">
<Grid Margin="5,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="130" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<RadioButton VerticalAlignment="Center" Content="Search English" IsChecked="{Binding Path=SearchEnglish}" />
<RadioButton VerticalAlignment="Center" Grid.Column="1" Content="Search International" IsChecked="{Binding Path=SearchInternational}" />
</Grid>
</Border>
</Grid>
</GroupBox>
This is the view model property to which the ListView is bound:
public ObservableCollection<DataAccess.TranslationItem> FoundItems
{
get
{
return p_FoundItems;
}
set
{
p_FoundItems = value;
NotifyOfPropertyChange("FoundItems");
TranslationsFound = string.Format("{0} translations found", p_FoundItems.Count);
}
}
This is the code that builds the FoundItems collection. It takes just a few ms to execute and build the collection. The speed issue is definitely not located here. It is using a black box DLL to get the list. It then builds a collection of items that can be used in the ObservableCollection.
public List<TranslationItem> SearchList(bool fCaseSensitive, bool fIgnoreAmpersands, bool fExactMatch,
string sSearchLanguage, string sSearchString)
{
List<TTranslations.TranslationItem> lstFound = null;
List<TranslationItem> lstReturn = new List<TranslationItem>();
p_trItems.SearchingFile += p_trItems_SearchingFile;
lstFound = p_trItems.SearchList(fCaseSensitive, fIgnoreAmpersands, fExactMatch, sSearchLanguage, sSearchString);
foreach (TTranslations.TranslationItem tiItem in lstFound)
lstReturn.Add(new TranslationItem(tiItem));
return lstReturn;
}
In the view model, I have the following code that is responsible for setting FoundItems. It casts the collection returned to an ObservableCollection.
FoundItems = new ObservableCollection<DataAccess.TranslationItem>(p_trItems.SearchList(p_fCaseSensitive, p_fIgnoreAmpersands, p_fExactMatch, p_fSearchEnglish ? "E" : "I", p_sSearchString));
A breakpoint before and after this line of code indicates that FoundItems takes mere milliseconds to build.
Any idea why it would be loading so slowly? This is a conversion from an older application where I'm building the list manually, in a loop, and that takes only a second or two load.
This ended up being an easy fix. I added the following to the ListView declaration:
ScrollViewer.CanContentScroll="True"
It now looks like this:
<ListView Grid.ColumnSpan="2"
ItemsSource="{Binding FoundItems}"
SelectionMode="Single"
MaxHeight="2000"
VirtualizingPanel.IsContainerVirtualizable="True"
VirtualizingPanel.IsVirtualizing="True"
ScrollViewer.CanContentScroll="True">
My list went from taking at least a minute to load large lists, to almost instantaneous.
Related
the issue is hard to explain but I will try my best. This code is part of a proof-of-concept for a webscraping application I am working on - only recently started.
I am attempting to display 2 columns of data using a DataGrid. I am able to display the data by assigning an ObservableCollection<> to the DataGrid.ItemsSource(see image below), the issue I am getting is that I have no control over column properties. I am very new to WPF and have tried to 'sort of' adopt MVVM.
In the picture it shows 4 columns, both the 'Title' and 'Price' from the ItemsSource are the 2 large ones.
private void ScrapeProductButton_OnClick(object sender, RoutedEventArgs e)
{
string url = this.ProductToScrapeUrlTextBox.Text;
scraper.ScrapeData(url);
var entries = scraper.Entries;
WebScrapedItems.ItemsSource = entries;
WebScrapedItems.Columns.Add(new DataGridTextColumn(){Header = "Title",
Width = new DataGridLength(0.8, DataGridLengthUnitType.Star)});
WebScrapedItems.Columns.Add(new DataGridTextColumn(){Header = "Price",
Width = new DataGridLength(0.2, DataGridLengthUnitType.Star)});
}
As seen in the code behind above, I tried to add 2 columns of the correct formatting assuming the contents of the ItemsSource would just do the same (obviously it isn't). Turning AutoGeneratedColumns off in the XAML doesn't display the ItemsSource but does display the other columns correctly.
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<Button Name="ScrapeProductButton" Grid.Column="0"
Margin="10 10 10 10" Background="AntiqueWhite"
Content="Scrape Website" FontWeight="Bold"
Click="ScrapeProductButton_OnClick"/>
<TextBox Name="ProductToScrapeUrlTextBox" Grid.Column="1"
Margin="10" Padding="2"/>
</Grid>
<DataGrid Height="350" Width="740" Margin="10" Name="WebScrapedItems"
VerticalAlignment="Center" HorizontalAlignment="Center"
RowHeight="30" ColumnWidth="390" IsReadOnly="True"
AutoGenerateColumns="True" FrozenColumnCount="2">
</DataGrid>
</StackPanel>
My goal is to just display the contents of the ItemsSource objects but such that they are correctly formatted. I feel like binding could be a potential reason its not working properly, but again I am still new to WPF and haven't started reading up on it yet.
Any help would be greatly appreciated, and any WPF advice, MVVM or anything would be great also.
Thanks!
I fixed the issue through binding to the field of the object that I wanted to display within the column. This mean I could remove the formatting the in ScrapeProductButton_OnClick method.
XAML code is here:
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="3*"/>
</Grid.ColumnDefinitions>
<Button Name="ScrapeProductButton" Grid.Column="0" Margin="10 10 10 10" Background="AntiqueWhite" Content="Scrape Website" FontWeight="Bold" Click="ScrapeProductButton_OnClick"/>
<TextBox Name="ProductToScrapeUrlTextBox" Grid.Column="1" Margin="10" Padding="2"/>
</Grid>
<DataGrid Name="WebScrapedItems" AutoGenerateColumns="False">
<DataGrid.Columns >
<DataGridTextColumn Header="Title" Binding="{Binding Title}" Width="3*"/>
<DataGridTextColumn Header="Price" Binding="{Binding Price}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
I appreciate the time people spent on looking through the problem - sorry if I wasted your time.
I have created sample app for demonstrating the issue.
Sorry its quite difficult to put all the code here since there are model classes, datamodel, service file which fetches the data from rest api.
So only few files are being included which gives information.
_placeList = await DataModel.PlaceDataSource.GetData(url); this line of statement from PlacePage.xaml.cs file is actually fetching records but doesn't get binded and displayed in listview.
But gridViewPlaces.ItemsSource = await DataModel.PlaceDataSource.GetData(url); works.
You can find the source code here. Project Download Link
MainPage.xaml
<SplitView x:Name="splitView" IsPaneOpen="True" OpenPaneLength="250" Grid.Row="1" DisplayMode="Inline">
<SplitView.Pane>
...
</SplitView.Pane>
<SplitView.Content>
<Grid>
<Frame x:Name="rootFrame" />
</Grid>
</SplitView.Content>
</SplitView>
PlacePage.xaml
<GridView Name="gridViewPlaces" ItemsSource="{x:Bind PlaceList}" SelectionMode="Single">
<GridView.ItemTemplate>
<DataTemplate>
<Grid Width="200" Height="Auto">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="Key" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Name}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="Value" />
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Value}" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
PagePage.xaml.cs file
private IEnumerable<Place> _placeList;
public IEnumerable<Place> PlaceList
{
get { return _placeList; }
}
public event EventHandler GroupsLoaded;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
url = e.Parameter.ToString();
LoadPlaces();
}
async private void LoadPlaces()
{
_placeList = await DataModel.PlaceDataSource.GetData(url);
//gridViewPlaces.ItemsSource = await DataModel.PlaceDataSource.GetData(url); // This works
gridViewPlaces.UpdateLayout();
if (GroupsLoaded != null)
GroupsLoaded(this, new EventArgs());
}
Your PlaceList property needs to fire notifications to let the binding know there’s a change. As is, when you replace _placeList you don't notify anybody that PlaceList changed and so nothing updates. The typical pattern here is to initialize the PlaceList property read only and then add things to that existing collection rather than swapping out the collection, though if you notify that you've swapped the collection that should work too.
Additionally, the IEnumerable inside PlaceList needs to provide notifications when its contents change. The standard way to do this is to make it an ObservableCollection since OC implements INotifyPropertyChanged and INotifyCollectionChanged for you. See theBinding to collections Quickstart
I have a Grid control defined in WPF...
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
Further down, I have defined an ItemsControl...
<ItemsControl Name="EntitlementsList" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{Binding Entitlements, Mode=TwoWay}"
Margin="0 10 0 3" AlternationCount="2">
<ItemsControl.Template>
...
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type Model:Entitlement}">
<Grid Margin="0 0 10 0" >
<Grid.Style>
...
</Grid.Style>
<Grid.ColumnDefinitions>
...
</Grid.ColumnDefinitions>
<ToggleButton Grid.Column="0"
HorizontalAlignment="Left" Margin="0 2 0 0"
VerticalAlignment="Bottom"
Style="{DynamicResource NotesToggleButton}"
CommandParameter="{Binding}"
Command="{Binding Path=DataContext.GetEntitlementDetails,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ItemsControl}}}" />
...
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This displays my Entitlements correctly.
The command for the ToggleButton is defined as...
private RelayCommand _getEntitlementDetails;
public RelayCommand GetEntitlementDetails
{
get
{
return _getEntitlementDetails ?? (_getEntitlementDetails = new RelayCommand(x =>
{
CurrentEntitlement = x as Entitlement;
}));
}
}
Then I have another ItemsControl that is collapsed until CurrentEntitlement is set...
<ItemsControl Name="ProductList" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="5" Height="150"
HorizontalAlignment="Stretch" BorderBrush="Transparent"
ItemsSource="{Binding ContextManager.CurrentContext.CurrentEntitlement.ProductList, Mode=TwoWay}"
Visibility="{Binding ContextManager.CurrentContext.CurrentEntitlement,
Converter={StaticResource NullVisibilityConverter}}">
<ItemsControl.Template>
...
</ItemsControl>
My item source is ProductList inside the Entitlements class...
private ObservableCollection<Product> _productList;
public ObservableCollection<Product> ProductList
{
get { return _productList; }
set
{
_productList = value;
OnPropertyChanged();
}
}
...where all the properties of "Product" raise OnPropertyChanged.
Now, I have been fully expecting my button to execute the command to set the CurrentEntitlement, make my second ItemsControl visible within the grid and display the ProductList, and stepping through the code shows that CurrentEntitlement does get set with a valid instance with items in the ProductList...but I still see nothing on the UI. I have been searching for a solution for 3 days, and I apologize if this is a duplicate question, but I am at my wit's end!
I can't see the issue. What am I doing wrong here?
Visual Studio 2015 Community, targeting 4.5, running on Windows 7 Enterprise SP1.
Thanks in advance!
I ended up finally figuring it out. There was a binding to some data that was written incorrectly that invalidated the second ItemsControl.
I am fairly new to WPF and MVVM and a newb in general, so thank you in advance for your patience.
I am using a custom class in my model, and I have an ObservableCollection of that custom object in my viewmodel. In the class' constructor, I am adding the object to the collection when it is instantiated. In my view, I am using a DataGrid that is bound to the collection to list all active instances of the class. I am trying to implement a drag-and-drop from the DataGrid onto a trash can icon that would allow a user to dispose of unneeded instances of the class.
The problem is that when you click anything in the DataGrid, the program immediately crashes with an ArgumentOutOfRange exception - ("The given DisplayIndex is out of range. DisplayIndex must be greater than or equal to 0 and less than Columns.Count." "Actual value was 0"). DisplayIndex seems to relate to the DataGrid column, so this exception is probably due to the fact that I am not displaying any columns in the traditional sense - in my DataGrid, AutoGenerateColumns is set to False, and I am displaying everything I need to display using a RowDetailsTemplate. (The reason for this is that the area where I am displaying the DataGrid is narrow, so I need a nested, item-specific grid to represent the item properly.) The DataGrid displays and syncs with the collection fine, but obviously has some issues. I have read dozens of links on DataGrid crashes, and haven't found anything involving this exception.
My desired behavior is to pass the custom object represented by the DataGrid item to a target when I drag and drop it. I don't care which "column" they clicked or anything else - I just need a way to pass either an object reference or a SelectedIndex (the items index in the collection) to a method in the viewmodel.
Thank you in advance for any help! The offending bit of code (XAML) seems to be:
<ScrollViewer DockPanel.Dock="Bottom" Margin="2" Width="180" ScrollViewer.VerticalScrollBarVisibility="Auto">
<DataGrid ItemsSource="{Binding Path=myCollection, Mode=OneWay}" AutoGenerateColumns="False" RowDetailsVisibilityMode="Visible" HeadersVisibility="None">
<DataGrid.RowDetailsTemplate>
<DataTemplate DataType="model:myClass">
<Border CornerRadius="10" Background="AliceBlue">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding MyString1}" FontSize="21" VerticalAlignment="Bottom" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding MyCustomProperty, Converter={StaticResource MyIValueConverter}}" VerticalAlignment="Bottom" />
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{Binding MyString2}" TextWrapping="Wrap" />
<Image Source="/Resources/image1.png" Grid.Column="2" Grid.Row="0">
<Image.DataContext>
<Properties:Resources/>
</Image.DataContext>
</Image>
<Image Source="/Resources/image2.png" Grid.Column="2" Grid.Row="1">
<Image.DataContext>
<Properties:Resources/>
</Image.DataContext>
</Image>
</Grid>
</Border>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
</ScrollViewer>
The issue was indeed because I am not generating any "traditional" columns. This is apparently a known bug, documented here: Microsoft Bug Report
As a workaround, I just defined an invisible column within the DataGrid and it seems to behave properly now:
<DataGrid.Columns>
<DataGridTemplateColumn Visibility="Hidden" />
</DataGrid.Columns>
I am trying to get a custom UserControl to render in a ListBox, but nothing is being rendered. I came across this question and solution which works for the simple example, but my situation is a little different. I have a PersonControl for a Person object and a CoupleControl that can reference two PersonControl controls.
I've tried a couple things in the CoupleControl which haven't worked. I commented out one of the ways:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Control:PersonControl Grid.Column="0"
x:Name="LeftPerson" />
<Control:PersonControl Grid.Column="1"
x:Name="RightPerson" />
<!-- This is how I'd like to do it in case I create other controls
I wish to replace the PersonControls (e.g. AnimalControl) -->
<!--<UserControl Grid.Column="0"
x:Name="LeftPerson" />-->
<!--<UserControl Grid.Column="1"
x:Name="RightPerson" />-->
</Grid>
The relevant WPF snippet for the list box:
<ListBox Grid.Row="1"
ItemsSource="{Binding Persons}">
<ListBox.ItemTemplate>
<DataTemplate>
<Control:CoupleControl />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In the code-behind:
public ObservableCollection<CoupleControl> Persons { get; private set; }
Person joe = new Person("Joe", "Smith", Person.SexType.Male);
Person jane = new Person("Jane", "Smith", Person.SexType.Female);
PersonControl joeControl = new PersonControl();
PersonControl janeControl = new PersonControl();
joeControl.DataContext = joe;
janeControl.DataContext = jane;
CoupleControl coupleControl = new CoupleControl();
coupleControl.LeftPerson.DataContext = joe;
coupleControl.RightPerson.DataContext = jane;
//coupleControl.LeftPerson.Content = joeControl; // Also doesn't work
//coupleControl.RightPerson.Content = janeControl; // Also doesn't work
Persons.Add(coupleControl);
Can someone help me get the CoupleControl to render in a ListBox?
Your approach is a bit too code-heavy for my taste, why not set DataContext in XAML?
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Control:PersonControl DataContext="{Binding LeftPerson}" />
<Control:PersonControl DataContext="{Binding RightPerson}" Grid.Column="1" />
</Grid>
Or maybe even drop UserControls altogether if they are not so complex? In this case using DataTemplates can be faster and simpler. Say we defined templates for Person and Couple in Window resources (Couple is just a class with LeftPerson and RightPerson properties):
<Window.Resources>
<DataTemplate x:Key="personTemplate" DataType="TestWPF:Person">
<Border BorderThickness="1" BorderBrush="Green" CornerRadius="5">
<StackPanel>
<TextBlock Text="{Binding FirstName}" />
<TextBlock Text="{Binding LastName}" Margin="3,0,0,0" />
</StackPanel>
</Border>
</DataTemplate>
<DataTemplate x:Key="coupleTemplate" DataType="TestWPF:Couple">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ContentControl Content="{Binding LeftPerson}"
ContentTemplate="{StaticResource personTemplate}" />
<ContentControl Content="{Binding RightPerson}"
ContentTemplate="{StaticResource personTemplate}" Grid.Column="1" />
</Grid>
</DataTemplate>
</Window.Resources>
Then you set ItemTemplate for your ListBox:
<ListBox Grid.Row="1" ItemsSource="{Binding Persons}" ItemTemplate="{StaticResource coupleTemplate}" />
This way you can make some more templates for the types you need and just set them in ListBox in one single line.