I realize this question could be boiled down to "Why is my code so slow?" but I'm hoping to get more out of that. Let me explain my code.
I have a class that implements INotifyPropertyChanged in order to do binding, and that class looks similar to this:
public class Employee : INotifyPropertyChanged
{
string m_strName = "";
string m_strPicturePath = "";
public event PropertyChangedEventHandler PropertyChanged;
public string Picture
{
get { return this.m_strPicturePath; }
set { this.m_strPicturePath = value;
NotifyPropertyChanged("Picture"); }
}
public string Name
{
get { return this.m_strName; }
set { this.m_strName = value;
NotifyPropertyChanged("Name");
}
}
private void NotifyPropertyChanged(String pPropName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(pPropName));
}
}
}
In my XAML I've created a DataTemplate that binds to this object:
<DataTemplate x:Key="EmployeeTemplate">
<Border Height="45" CornerRadius="0" BorderBrush="Gray" BorderThickness="0" Background="Transparent" x:Name="bordItem">
<Grid Width="Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Name}" VerticalAlignment="Center" Padding="10" HorizontalAlignment="Stretch" FontWeight="Bold" FontSize="20"/>
<Image Grid.Column="1" Source="{Binding Path=Picture}"></Image>
</Grid>
</Border>
</DataTemplate>
and then put this template on a ListBox:
<ListBox x:Name="lstEmployees" ItemTemplate="{DynamicResource EmployeeTemplate}" VirtualizingStackPanel.VirtualizationMode="Recycling" VirtualizingStackPanel.IsVirtualizing="True"></ListBox>
So in code it's set as:
lstEmployees.ItemsSource = this.m_Employees;
the "m_Employees" list gets hydrated at app startup from a database, and then after that happens I set the above line of code. The ListBox is on a TabControl.
Now, my actual problem: My "m_Employees" list is returning about 500+ employees from the database, so the collection is slightly big. I get a performance hit in WPF only when the application first starts up and someone navigates to that tab with the ListBox on it. The UI freezes for about 3 seconds, but only when the app first starts up - afterwards it's fine.
Could this be because:
The code has to hit the hard drive to go find the image of each employee?
I am doing Virtualizing incorrectly?
EDIT
WPF is doing the rendering using my DataTemplate once, only when someone navigates to that TabControl, and is suddenly trying to draw 500+ employee items? If so, is there any way to "preload" the ListView in WPF?
Any other suggestions for improving the above would be apprecated. Thanks for reading and for any advice ahead of time.
-R.
Wrap m_Employees with a public
property (Employees)
Instead of setting your ItemsSource in the code like you do, set it with Binding and set IsAsync to
True.
ItemsSource="{Binding Empolyess, IsAsync=True}"
You can also assign the Binding in the code.
Hope this helps.
The perf of your query is definitely suspect. If you want it to perform better, you can do any number of lazy initialization techniques to get it to run faster.
The easiest option would be to start with an empty enumeration, and only populate it at a later time.
The obvious way to do this would be to add a "Query" or "Refresh" button, and only freeze up the app when the user clicks it.
Another simple option is to queue a background task/thread to do the refresh.
If you are more concerned about consistent perf/super-responsive UI, then you should try to do more granular queries.
I am not sure if WPF handles virtualization of the items (only pulls from the enumeration when each item comes into view), but if it does, you could do paging/yield returns to feed ItemsSource.
If WPF just grabs the whole enumeration at once, you could still do smaller lazy-eval/paging, if you can determine which items are in view. Just populate the object with "zombie" items, and when they come into view, perform the query, and update the properties on the individual item.
Related
I'm developing a WPF app using MVVM where I show error messages/special dialogs via a lightbox styled pop-up. These sub-Views are User Controls displayed by a ContentControl in the main View.
Up until now, each sub-View has been hardcoded to perform a single function (say, display error or ask the user to backup first). But as the app progresses, I'm seeing the the same design pattern from most of these controls:
Icon image in top left corner
Heading text next to icon
Message text in the middle
2 buttons in the bottom right corner
With MVVM I should be able to abstract this pattern and reuse this control for displaying errors, asking the user to back up, anything really, just by binding. I should even be able to bind the names of the buttons or even hide 1 if it isn't used...stuff like that.
But should I? Is there a performance benefit/hit from doing it like this? Seems like this would fall under DRY when there's 8 sub-views all with the same grid pattern.
Dry is not about performance.
It's about saving you time writing code and in the maintenance phase.
Whilst it would be more elegant to make one generic re-usable window this probably comes at some sort of cost.
Does the work cost you more than the benefit you get? The decision whether to rationalise into one probably-more-complicated view or not should be based on a sort of cost benefit analysis.
Factors you should consider:
How long does it take to make each view?
How complicated is the functionality in each?
How much effort is necessary to make a generic?
How many exceptional cases are there and how much would they complicate making this generic?
Would making this generic obscure functionality and to what extent is it going to make maintenance more expensive?
How likely is it you'll have to change the look of these things?
If you're highly unlikely to change the look, there are a few edge cases make a generic view complicated and injecting your functionality has complications then just copying and pasting markup into each view makes some sense.
Edit:
Remember that styling is re-usable.
Here's a concrete bit of markup to consider.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<Path Data="{Binding IconGeometry}"
Stretch="Fill"
Fill="Black"
Height="28"
Width="28"/>
<TextBlock Text="{Binding Heading}"/>
</StackPanel>
<TextBlock Grid.Row="1"
Text="{Binding Message}"/>
<ItemsControl
Grid.Row="2"
ItemsSource="{Binding NamedCommandCollection}"
HorizontalAlignment="Right">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Content="{Binding ButtonText}" Command="{Binding ButtonCommand}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
You present a viewmodel to this.
This viewmodel implements inotifypropertychanged and provides a string property for the heading, message etc.
The less-obvious things here are a path with a geometry rather than image. This depends on what your iconography will look like exactly but the simple one colour shape is very common now.
You can define geometries in a resource dictionary, grab the appropriate one out of there and supply it as a property. Merged resource dictionaries go in application.current.resources which is pretty much an in memory dictionary of objects keyed by a string of your x:key.
The buttons are produced by an itemscontrol which templates out it's items into a horizontal line of buttons.
Build a viewmodel representing a button.
string property for name and a relaycommand or delegatecommand for the ButtonCommand.
Let's call that a ButtonVM.
Add a ButtonVM to an observablecollection property NamedCommandCollection and you get a button appears. Add one, two, three. However many you like.
You could make the ButtonVM just take a relaycommand you build and supply or have one itself and you inject an action. You can capture variables as you build an action dynamically.
Command also has canexecute. You can use that to refine when a button can be clicked or not. EG I have a property IsBusy in a base viewmodel which I use to flag whether any command is "running" to obviate that very fast double click breaking everything.
Here it is:
public class BaseViewModel : INotifyPropertyChanged
{
private bool isBusy;
[IgnoreDataMember]
public bool IsBusy
{
get => isBusy;
set => ToVal(ref isBusy, value, nameof(IsBusy));
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public void ToObj<T>(ref T backer, T value, [CallerMemberName] string propertyName = null)
{
backer = value;
this.RaisePropertyChanged(propertyName);
}
public void ToVal<T>(ref T backer, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(backer, value))
{
return;
}
backer = value;
this.RaisePropertyChanged(propertyName);
return;
}
}
Icommand has a canexecute bool and will disable a control a command is bound to if it's false. However, this relies on commandmanager deciding to requery canexecute and disable that control. There are circumstances when this won't happen fast enough. Hence it's best to use the bool to guard the code in a command.
A fairly random example out some real code:
private RelayCommand newMapCommand;
public RelayCommand NewMapCommand
{
get
{
return newMapCommand
?? (newMapCommand = new RelayCommand(
() =>
{
if (IsBusy)
{
return;
}
ResetMap();
IsBusy = false;
},
( ) => !IsBusy
));
}
}
Relaycommand is in mvvmlight. Since I work in net core nowadays and there's a dependency in commandwpf on net old, I grabbed the source for the bits I want of mvvmlight. I retain the namespaces since Laurent will probably eventually address this or net 5 may obviate the issue.
A usercontrol can itself contain a usercontrol. If you wanted flexibility then it could have a contentcontrol and template out what is bound to it's content.
This is used for viewmodel first, a common way to switch out content for navigation etc. I wrote an example to explain the evils of pages :^)
https://social.technet.microsoft.com/wiki/contents/articles/52485.wpf-tips-and-tricks-using-contentcontrol-instead-of-frame-and-page-for-navigation.aspx
My data from an ObservableCollection only sometimes displays on my ListView. If I restart the app, the data displays fine. Sometimes when I navigate away from the page and go back, the data will sometimes display and other times not. It seems to be random.
Here is my XAML code:
<ScrollViewer Grid.Row="2" Margin="0,42,0,0">
<Grid>
<ListView ItemsSource="{x:Bind collection, Mode=OneWay}" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" IsItemClickEnabled="True" SelectionChanged="MySelectionChanged" Visibility="Visible">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:ObjectName">
<TextBlock Text="{x:Bind Data0, Mode=OneWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView ItemsSource="{x:Bind collection, Mode=OneWay}" HorizontalAlignment="Left" Margin="375,0,0,0" VerticalAlignment="Top" SelectionMode="None" Visibility="Visible">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:ObjectName">
<TextBlock Text="{x:Bind Data1, Mode=OneWay}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</ScrollViewer>
I tried doing a Debug.WriteLine() on all the data that should be displayed, and the debug output successfully prints it every time. There's only a problem when it comes to displaying this data on the ListView.
EDIT: Here's the corresponding xaml.cs code:
private ObservableCollection<ObjectName> collection;
private List<ObjectName> sorted;
private ObjectName clicked = new ObjectName();
public MainPage()
{
GetSave();
this.InitializeComponent();
}
private async void GetSave()
{
DataStorage ds = new DataStorage();
await ds.DeserializeObjectAsync();
collection = ds.ObjectName;
if (collection != null)
{
sorted = new List<ObjectName>(collection);
sorted.Sort((x, y) => string.Compare(x.Data0, y.Data0));
collection = new ObservableCollection<ObjectName>(sorted);
}
}
I tried InitializeComponent() before and after the logic, and it gives the same result.
Based on the xaml.cs code you provided, the problem looks like this:
First, you are not awaiting an async call.
Second, you are replacing the ObservableCollection.
The solution:
Don't call async code from your page's constructor. Async calls are
called async for a reason and trying to 'hide' their true nature by
omitting the await keyword when calling them won't work. Change GetSave()'s
return value to Task, override the OnNavigatedTo() method (read more
about it in it's documentation) of MainPage and await GetSave()
inside that. This alone won't solve your problem though.
Never replace an ObservableCollection. I remember banging my head on the
wall multiple times because of this. What you need to understand is
that when you are binding to an object in XAML, a dedicated Binding
object is created that links the source (in your case, the
ObservableCollection) and the target (the ListView) together. In
your code, initially 'collection' is set to null. When your MainPage
is created, the Binding object is created as well and it binds that
null value to your ListViews' Source property. Later, when your
async initialzation code is finished, you replace that null value
with an actual ObservableCollection, but the ListViews are not
notified about that, they are only looking for changes in the
collection's items that they are bound to, they are not prepared for
handling the situation when the collection itself is swapped
under them. So what you can do to fix this problem is: only create
your ObservableCollection instance in MainPage's constructor (or at
declaration - matter of taste in this case) and in GetSave() first
call collection.Clear() and then add your items to it with
collection.Add().
What currently happening in your code is that in some cases you are replacing the ObservableCollection before the Binding object is created and sometimes after that, so that's why it looks like you app's behavior is totally random - because actually it is. :) By adding the modifications I suggested above, you'll make sure that the Binding object is bound to the ObservableCollection you created in the constructor (empty at the time of the binding), and then you are initializing that collection after MainPage is already loaded, so your ListViews are getting notified about the changes.
So, I made a really simple attempt to try out data binding from a property of a class that I have, but, for whatever reason, the code actually do anything. It's not throwing any errors, but something must not be working right. I'm just currently testing if it'll behave like I want it to, which, in this case, will set the opacity of a rectangle to zero. Here's the xaml for the Data Template that doesn't seem to want to respond correctly:
<HubSection x:Name="China" Width="440" Height="460" Background="#FF343434" Header="China" IsHeaderInteractive="True" Tapped="{x:Bind HubSectionTapped}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="50,0,0,0">
<DataTemplate x:DataType="data:MainPageView">
<Grid Height="460" Width="410" VerticalAlignment="Bottom" x:Name="ChinaBackground">
<Image Source="Assets/chinaFlag.bmp" x:Name="ChinaFlag"/>
<Rectangle x:Name="ChinaSelected_Rect" Width="410" Height="30" VerticalAlignment="Bottom" Fill="BlueViolet" Opacity="{x:Bind Opacity1}"/>
</Grid>
</DataTemplate>
</HubSection>
And here's the code behind:
public MainPageView TheMainPageView;
public MainPage()
{
this.InitializeComponent();
timer = new DispatcherTimer();
timer.Tick += Timer_DyanmicResize;
timer.Tick += Timer_SelectionIndicator;
timer.Start();
TheMainPageView = new MainPageView ();
}
And finally, here's the class MainPageView that's referenced:
public class MainPageView
{
public int Opacity1 {get; set;}
public int Opacity2 {get;set;}
public int Opacity3 { get; set; }
public MainPageView()
{
this.Opacity1 = 0;
this.Opacity2 = 0;
this.Opacity3 = 0;
}
}
In the XAML I included the xmlns:data="using:TestApp.Models" (models is the folder in which the class MainPageView is housed). As I said, it's not throwing errors, but it's not doing anything either, so I'm a bit at a loss of where to start addressing this because there aren't any errors to trace back. Thanks in advance for any help you guys can provide
HubSection uses a DataTemplate to define the content for the section, content can be defined inline, or bound to a data source. When using binding in this DataTemplate, we need set DataContext property of HubSection to provide data source for the DataTemplate.
{x:Bind} does not use the DataContext as a default source—instead, it uses the page or user control itself. So it will look in the code-behind of your page or user control for properties, fields, and methods.
This is right when you use {x:Bind} directly in page or user control. While Inside a DataTemplate, there is a little difference.
Inside a DataTemplate (whether used as an item template, a content template, or a header template), the value of Path is not interpreted in the context of the page, but in the context of the data object being templated. So that its bindings can be validated (and efficient code generated for them) at compile-time, a DataTemplate needs to declare the type of its data object using x:DataType.
For more information about Data binding in UWP, please check Data binding in depth.
To fix your issue, you just need to set DataContext in HubSection like following:
<HubSection x:Name="China" Width="440" Height="460" Background="#FF343434" Header="China" IsHeaderInteractive="True" Tapped="{x:Bind HubSectionTapped}" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="50,0,0,0" DataContext="{x:Bind TheMainPageView}">
<DataTemplate x:DataType="data:MainPageView">
<Grid Height="460" Width="410" VerticalAlignment="Bottom" x:Name="ChinaBackground">
<Image Source="Assets/chinaFlag.bmp" x:Name="ChinaFlag"/>
<Rectangle x:Name="ChinaSelected_Rect" Width="410" Height="30" VerticalAlignment="Bottom" Fill="BlueViolet" Opacity="{x:Bind Opacity1}"/>
</Grid>
</DataTemplate>
</HubSection>
Here when using {x:Bind} in HubSection, it uses the page itself as its data source as HubSection is in the page directly. So it can get TheMainPageView field in the code-behind. But for the {x:Bind} in DataTemplate, it can't as
its data source is the data object being templated not the page. So we need to provide this data object by setting DataContext property of HubSection.
Check you output window for errors but I imagine you might see a binding error in there. Opacity is a double, you are using an int so will get a type conversion error.
I am working on a windows store 8.1 app, I have added Grids in MainPage.xaml using List in MainPage.xaml.cs
MainPage.xaml
<GridView Margin="20" x:Name="main" SelectionMode="None" IsItemClickEnabled="True" ItemClick="main_ItemClick">
<GridView.ItemTemplate>
<DataTemplate>
<Grid Background="Red" Width="250" Height="200">
<Grid.RowDefinitions>
<RowDefinition Height="150"/>
<RowDefinition Height="2*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Image Grid.Row="0" Stretch="UniformToFill" Source="{Binding ImageLocation}"/>
<TextBlock Text="{Binding Title}" Grid.Row="1" FontSize="28" />
<TextBlock Text="{Binding SubTitle}" Grid.Row="2" FontSize="16" />
</Grid>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
MainPage.xaml.cs
protected override void OnNavigatedTo(NavigationEventArgs e)
{
List<data> myList = new List<data>();
myList.Add(new data()
{
ImageLocation = #"Assets/network.png",
iName = "NetWork",
SubTitle ="Network",
Title = "Network"
});
myList.Add(new data()
{
ImageLocation = #"Assets/fb.png",
iName = "Facebook",
SubTitle = "Facebook",
Title = "Facebook"
});
main.ItemsSource = myList;
}
private void main_ItemClick(object sender, ItemClickEventArgs e)
{
Frame.Navigate(typeof(ListView));
}
I want that when someone click on any of the grids, a TextBlock in ListView page show which grid was clicked in MainPage .
This will be a challenge to explain without showing you in code, but here goes...
Hopefully you have created two pages so far. MainPage.xaml that holds your GridView. And a DetailsPage.xaml that will have the layout to show one item.
In the code-behind of MainPage.xaml, like you have in your sample code, you handle the ItemCLick of the GridView, but you want to get the Id of the item clicked, not the item itself. The reason for this is that you want to pass a string, and not a complex object.
In your handler, the event args (e) has a property called ClickedItem that will be the item you are binding to. Let's pretend it's a UserObject you are binding to. In your handler do something like this:
var user = e.ClickedItem as UserObject;
this.Frame.Navigate(typeof(DetailPage), user.Id.ToString());
So, what's happening here? Almost the same code you had before. Except you are navigating to the type of the second page instead of anything else. You are also passing in (the second argument in the Navigate method) the exact record you want to show.
Then in your DetailPage.xaml code-behind you ned to override the OnNavigatedTo method. This method is what is invoked when the Navigation framework directs to the page. It's has a NavigationPararmeter passed to it that you can use to extract the key you passed.
I think it's actually args.Parameter you want to use. You can parse it to an integer and use that to fetch the individual record you have somehow in memory in your application.
var id = int.Parse(args.Parameter);
var user = YourFactory.GetUser(id);
The reason I shifted from this is how you do it to "I think this is how it works" is because although the basic framework operates like this, most developers do not use it like this. Most developers implement something like Prism.StoreApps which introduces not only a lightweight MVVM framework, but also a sophisticated NavigationService that lets you inject parameters directly into an auto-associated view model.
But based on the simplicity of your question, try not to pay attention to that last bit. I explained the basic workflow using the in-box framework. It works just fine, and it will get the job done. When you are ready to write a more advanced implementation you can investigate Prism.StoreApps
More info: http://msdn.microsoft.com/en-us/library/windows/apps/xx130655.aspx
Best of luck!
So, I have a ListBox which is bound to a list of business objects, using a DataTemplate:
<DataTemplate x:Key="msgListTemplate">
<Grid Height="17">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding MaxWidth}" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Foreground="Silver" Text="{Binding SequenceNo}" />
<TextBlock Grid.Column="1" Text="{Binding MessageName}" />
</Grid>
</DataTemplate>
<ListBox Name="msgList"
Grid.Column="0"
ItemTemplate="{StaticResource msgListTemplate}"
SelectionChanged="msgList_SelectionChanged"
VirtualizingStackPanel.IsVirtualizing="True"
ScrollViewer.HorizontalScrollBarVisibility="Hidden">
</ListBox>
Sometime after binding, I want to colour certain items in the list to distinguish them from the others. I do this on a background thread:
if(someCondition)
{
msgList.Dispatcher.BeginInvoke(new Fader(FadeListItem), DispatcherPriority.Render, request);
}
delegate void Fader(GMIRequest request);
void FadeListItem(GMIRequest request)
{
ListBoxItem item =
msgList.ItemContainerGenerator.ContainerFromItem(request) as ListBoxItem;
if(item!=null)
item.Foreground = new SolidColorBrush(Colors.Silver);
}
This all works fine, and some list items are greyed out as expected. However, if I scroll such that the greyed items are no longer shown, then scroll back again to where they were, they are no longer silver, and have returned to the default black foreground.
Any idea why this is, or how to fix it? Is it because I have set IsVirtualizing to true? The listbox typically contains many items (20,000 is not uncommon).
Is it because I have set IsVirtualizing to true? The listbox typically contains many items (20,000 is not uncommon).
You nailed it - the item you set the foreground color on is getting trashed once the user scrolls away.
While you've got the right general idea, the way you're going about this is a very un-WPFy way to do this - one better way to do this is to have a bool DP in your business object class (or have the BO implement INotifyPropertyChanged), then bind the bool to the Foreground color via a custom IValueConverter that returns (isTrue ? whiteBrush : greyBrush).
Since you may not want to / may not be able to modify your business object to support INotifyPropChanged, this is the reason for the M-V-VM pattern - create a class that wraps the object that is a DependencyObject and exposes just the properties you're interested in displaying.