I am creating a little application that has one feature to show list of people in popup when user click on button. That list, that is in popup, can be pretty big, it can have as many as 3k entries. I am using virtualization, and I don't have problem with performance when list is drawn. But, when user open application for the first time and click on button for popup with list of people, it can take 2-5 seconds before popup is shown. After, that if user try to open popup again popup will be open without delay.
So my question is could I say to ListView to prepare items while it is not shown. Because there is very big chance that user will use application for quiet some time before it will need this popup.
Can I optimize this in some other way? Is there some collection that is better for this purpose in WPF?
Also people collection will be populated when page is loaded, and popup is at beginning closed.
Here it is code that I have:
public class AddressBookViewModel : BaseViewModel
{
...
private ObservableCollection<PeopleModel> people;
...
public ObservableCollection<PeopleModel> People
{
get { return people; }
}
}
public class PeopleModel
{
public PeopleModel(string address, string name)
{
Address = address;
Name = name;
}
public string Name{ get; set; }
public string Address { get; set; }
}
<Button x:Name="btnChoosePerson"
Command="{Binding TogglePeopleAddressPopupCommand}"
Content="..." />
<Popup MaxHeight="520"
IsOpen="{Binding ShowPeopleAddressPopup}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=btnChoosePerson}"
StaysOpen="False">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView
Grid.Row="2"
Width="500"
HorizontalAlignment="Left"
ItemsSource="{Binding People}"
SelectionMode="Single"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.CacheLength="10"
VirtualizingPanel.CacheLengthUnit="Item">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
HorizontalAlignment="Left"
Text="{Binding Address}" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Right"
Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Popup>
Related
I am relatively new in WPF and am trying to understand the MVVM pattern and how data-binding works with ObservableCollection, in order to build the application I am working on with MVVM. I have created a sample of my application that has a MainWindow where, depending on which button the user presses, a different View (UserControl) is displayed. The general idea is that the user will have access to the data of some elements from a database (e.g.: Customers, Products, etc.) and will be able to add new and edit, or delete, existing ones.
So, there is a CustomerView, with its CustomerViewModel, and a ProductView, with its ProductViewModel respectively. Also, there are two classes (Customer.cs & Product.cs) that represent the Models. The structure of the project is displayed here.
The MainWindow.xaml is as follows:
<Window.Resources>
<DataTemplate DataType="{x:Type viewModels:CustomerViewModel}">
<views:CustomerView DataContext="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:ProductViewModel}">
<views:ProductView DataContext="{Binding}"/>
</DataTemplate>
</Window.Resources>
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20*"/>
<ColumnDefinition Width="80*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center">
<Button x:Name="btnCustomers" Click="btnCustomers_Click" Content="Customers" Width="80" Height="50" Margin="10"/>
<Button x:Name="btnProducts" Click="btnProducts_Click" Content="Products" Width="80" Height="50" Margin="10"/>
</StackPanel>
<Grid Grid.Column="1">
<ContentControl Grid.Column="0" Content="{Binding}"/>
</Grid>
</Grid>
and the code behind MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public CustomerViewModel customerVM;
public ProductViewModel productVM;
public MainWindow()
{
InitializeComponent();
}
private void btnCustomers_Click(object sender, RoutedEventArgs e)
{
if (customerVM == null)
{
customerVM = new CustomerViewModel();
}
this.DataContext = customerVM;
}
private void btnProducts_Click(object sender, RoutedEventArgs e)
{
if (productVM == null)
{
productVM = new ProductViewModel();
}
this.DataContext = productVM;
}
}
Finally, the CustomerView.xaml is as follows:
<UserControl.Resources>
<viewModel:CustomerViewModel x:Key="customerVM"/>
<!-- Styling code here...-->
</UserControl.Resources>
<Grid DataContext="{StaticResource ResourceKey=customerVM}">
<Grid.RowDefinitions>
<RowDefinition Height="2*"/>
<RowDefinition Height="7*"/>
<RowDefinition Height="3*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBlock Text="Customers" FontSize="18"/>
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*"/>
<ColumnDefinition Width="5*"/>
</Grid.ColumnDefinitions>
<ComboBox x:Name="cmbCustomers" Grid.Column="0" VerticalAlignment="Top"
IsEditable="True"
Text="Select customer"
ItemsSource="{Binding}"
DisplayMemberPath="FullName" IsSynchronizedWithCurrentItem="True">
</ComboBox>
<StackPanel Grid.Column="1" Margin="5">
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0" Text="Id:" />
<TextBlock Grid.Column="1" x:Name="txtId" Text="{Binding Path=Id}" FontSize="16"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0" Text="Name:" />
<TextBlock Grid.Column="1" x:Name="txtFirstName" Text="{Binding Path=FirstName}" FontSize="16"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Grid.Column="0" Text="Surname:" />
<TextBlock Grid.Column="1" x:Name="txtLastName" Text="{Binding Path=LastName}" FontSize="16"/>
</StackPanel>
</StackPanel>
</Grid>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center">
<Button x:Name="btnAddNew" Content="Add New" Click="btnAddNew_Click"/>
<Button x:Name="btnDelete" Content="Delete Customer" Click="btnDelete_Click"/>
</StackPanel>
</Grid>
and the CustomerViewModel.cs:
public class CustomerViewModel : ObservableCollection<Customer>
{
public CustomerViewModel()
{
LoadCustomers();
}
private void LoadCustomers()
{
for (int i = 1; i <= 5; i++)
{
var customer = new Customer()
{
Id = i,
FirstName = "Customer_" + i.ToString(),
LastName = "Surname_" + i.ToString()
};
this.Add(customer);
}
}
public void AddNewCustomer(int id)
{
var customer = new Customer()
{
Id = id,
FirstName = "Customer_" + id.ToString(),
LastName = "Surname_" + id.ToString()
};
Add(customer);
}
}
Please note that the ProductView.xaml & ProductViewModel.cs are similar.
Currently, when the user presses the "Customers" or the "Products" button of the
MainWindow, then the respective View is displayed and the collections are loaded
according to the LoadCustomers (or LoadProducts) method, which is called by the
ViewModel's constructor. Also, when the user selects a different object from the
ComboBox, then its properties are displayed correctly (i.e. Id, Name, etc.). The
problem is when the user adds a new (or deletes an existing) element.
Question 1: Which is the correct and best way to update a changed Observable
Collection of an element and reflect its changes in the UI (Combobox, properties, etc.)?
Question 2: During testing this project I noticed that the constructor of the
ViewModels (consequently the LoadCustomers & LoadProducts method) are called twice. However, it is only called when the user presses the Customers or the
Products button respectively. Is it also called via the XAML data binding? Is
this the optimum implementation?
Your first question is basically a UX one, there is no correct or "best" way. You'll definitely end up using some sort of ItemsControl, but which one depends heavily on how you want your users to interact with it.
To your second question, you have a few mistakes in your code:
<viewModel:CustomerViewModel x:Key="customerVM"/> Instantiates a new view model, apart from the one that the main application created
Grid DataContext="{StaticResource ResourceKey=customerVM}" Then uses this "local" view model, ignoring the inherited one from the main application
That's why you see the constructor fire twice, you are constructing two instances! Eliminate the local VM and don't assign the DC on the grid. Other issues:
<views:ProductView DataContext="{Binding}"/> The DataContext assignment is total unnecessary, by virtue of being in the data template it's data context is already set up
<ContentControl Grid.Column="0" Content="{Binding}"/> Yuck, you should have a "MainViewModel" with a property that this uses. Don't make it be the whole data context
Lack of commands for your button clicks (related to the bullet above)
There is 3 kinds of Change Notification you need with Lists in MVVM:
Change Notificataions on every property of the list items.
Change Notification on the property exposing the list, in case the whole instance has to be replaced (wich is common because of 3)
Change Notification if elements are added to or removed from the collection. That is the only thing ObservableCollection takes care off. Unfortunately there is no Addrange option, so bulk operations wil lsmwap the GUI with Notifications. That is what Nr. 2 is there for.
As advanced option, consider exposing the CollectionView rather then the raw Collection. WPF GUI elements do not bind to raw Collections, only CollectionViews. But if you do not hand them one, they will create one themself.
I have a list of strings that I want to display on a menu. I used a Listbox and it works just that it won't let me highlight or copy/paste.
Here is my XAML
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="500"/>
<ColumnDefinition Width="500"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="450"/>
<RowDefinition Height="318"/>
</Grid.RowDefinitions>
<ListBox Grid.Row="1" Grid.Column="1" x:Name="uiOCRData" />
</Grid>
Heres what I have in C#
List<string> lines = new List<string>();
uiOCRData.ItemsSource = lines;
Thanks for the help!
You must use a ListBox.ItemTemplate so that you can include a control inside your ListBox.
Since you want to be able to select text etc., the best option is to use a TextBox.
<ListBox Grid.Row="0" Name="uiOCRData">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text="{Binding Path=.}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
EDIT
Let's say you want to bind to a list of some class objects instead of a simple list of strings. Say your class looks like this:
public class Data
{
public int Id { get; set; }
public string Name { get; set; }
}
Then you can bind to any one of chosen Properties of the class like this:
<ListBox Grid.Row="0" Name="uiOCRData">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Width="100" Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
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.
<ListView Name="myList" Background="Transparent" Margin="15,88,15,15">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<Grid Height="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Margin="5">
<TextBlock Name="xName" Text="{Binding Name}" FontSize="30" Margin="10,0,5,0" FontWeight="Medium"/>
<TextBlock Name="xNo" Text="{Binding No}" FontSize="25" Margin="10,0,5,0" TextTrimming="CharacterEllipsis"/>
</StackPanel>
<Grid Grid.Column="1" Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<RadioButton Grid.Column="0" Name="r1" Content="1" Width="10" Margin="5,15,-2,5"/>
<RadioButton Grid.Column="1" Name="r2" Content="2" Width="10" Margin="5,15,-2,5"/>
</Grid>
</Grid>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
Now I want to retrieve each item in ListView i.e, the controls in it. The TextBlocks content and to know which RadioButton is selected.
I tried taking classes like this
public class Att
{
public string Name;
public string No;
public RadioButton r1;
public RadioButton r2;
}
There are separate sources for the content of the TextBoxes in the ListView. They come from different classes. I tried retrieving the items using above class Att.
But it is showing some type conversion errors. Please anyone help me! I'm a beginner.
Thanks is advance.
Thanks for all the support!
Well I tried doing this on my own and finally I got it. What I really wanted is to know which RadioButton is checked. I tried taking class Att for retrieving the items in ListView. I made modifications to the Att Class and added properties r1 and r2 of type bool instead of RadioButton and binded this property to the IsChecked property of the RadioButton in XAML and also mode of binding is TwoWay. So the changes done in the XAML can be reflected in the source list of items.