A project I'm working on has some rather complex XAML that is noticeably affecting visual performance. Quite a few controls are collapsed for the initial state; however, since their XAML is parsed and visual /logical trees built, it's very slow to show what amounts to an almost blank object.
It looks like (and would like confirmation here) that using a ContentControl with an initial state of Collapsed and then embedding the desired control as a DataTemplate for that ContentControl, will defer loading of the desired control in the DataTemplate until the ContentControl is made visible.
I've built a generic DeferredContentControl that listens for the LayoutUpdated event of the main UI control (in general whatever element it is that I want to appear quickly), and when the first LayoutUpdated event of that UIElement fires, I used the Dispatcher to flip the visibility of the DeferredContentControl to true, which causes the control in the DeferredContentControl's DataTemplate to instantiate. By the time the user has reacted to the initial view of the screen (which is now fast), the "slow to load" (but still collapsed) control in the data template is ready.
Does this seem like a sound approach? any pitfalls? It seems to work well in testing both for Silverlight and WPF, and while it doesn't make things any faster it gives the perception of being as much as 50% snappier in my specific scenario.
I had the same problem (in a Silverlight project), and solved it in nearly the same way. It proved to be working as expected, have not encountered any pitfalls yet.
When you need to control the point in time when xaml is parsed and view elements are instantiated you can always use DataTemplates (not necessarily in cunjuction with ContentControl). You can call DataTemplate.LoadContent() to instatiate it, you don't have to switch the visibility of a ContentControl (although internally this will result in such a LoadContent call).
Have a look at my implementation if you want, it can even display a static text message while the heavier VisualTree is build:
<DeferredContent HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<DeferredContent.DeferredContentTemplate>
<DataTemplate>
<MyHeavyView/>
</DataTemplate>
</Controls:DeferredContent.DeferredContentTemplate>
<TextBlock Text="Loading content..."/>
</Controls:DeferredContent>
and the code
public class DeferredContent : ContentPresenter
{
public DataTemplate DeferredContentTemplate
{
get { return (DataTemplate)GetValue(DeferredContentTemplateProperty); }
set { SetValue(DeferredContentTemplateProperty, value); }
}
public static readonly DependencyProperty DeferredContentTemplateProperty =
DependencyProperty.Register("DeferredContentTemplate",
typeof(DataTemplate), typeof(DeferredContent), null);
public DeferredContent()
{
Loaded += HandleLoaded;
}
private void HandleLoaded(object sender, RoutedEventArgs e)
{
Loaded -= HandleLoaded;
Deployment.Current.Dispatcher.BeginInvoke(ShowDeferredContent);
}
public void ShowDeferredContent()
{
if (DeferredContentTemplate != null)
{
Content = DeferredContentTemplate.LoadContent();
RaiseDeferredContentLoaded();
}
}
private void RaiseDeferredContentLoaded()
{
var handlers = DeferredContentLoaded;
if (handlers != null)
{
handlers( this, new RoutedEventArgs() );
}
}
public event EventHandler<RoutedEventArgs> DeferredContentLoaded;
}
Related
I encountered an interesting situation in my MEF application. The main workspace is a region registered on a ContentControl with only one active view at a time. Setup and navigation is working just fine. Now what I observed is that when I'm changing the view on this MainRegion all my bindings to the viewmodel are evaluated again.
To check this, I added a counter on my view model to see how often it is loaded. When I'm changing View A -> View B -> View A, then the counter will be 3:
entering View A
leaving View A
entering View A
I'm using the normal call to activate a region:
region.Activate(view);
When debugging this issue I saw that when activating a region, the old one gets deactivated, eventually setting the
ContentControl.Content = null;
This seems to modify the visual tree and reevaluates all bindings on the old view.
It seems to be a mixture of a MEF and WPF problem. Is there any way to prevent the evaluation of bindings when activating a new region or on the WPF side prevent re-evaluation of bindings when ContentControl.Content becomes null?
I found a similar question but without an answer here:
WPF: disable bindings update on detach
This issue seems to be a problem of the ContentControl itself, see this post for further information: http://blogs.microsoft.co.il/tomershamam/2009/09/11/wpf-performance-sweets-contentcontrolcontent-null/
To avoid performance issues while switching between views in a regions you can keep the view in the visual tree by using another control for the region. Replace the ContentControl by a modified ItemsControl. I find a solution in this post: https://vslepakov.blogspot.de/2014/09/navigate-faster-with-prism-and-wpf.html. It works with hiding old views and show new views.
I modified this example like this:
public class RegionItemsControl : ItemsControl
{
protected override bool IsItemItsOwnContainerOverride(object item)
{
return false;
}
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
base.PrepareContainerForItemOverride(element, item);
((ContentPresenter)element).ContentTemplate = ItemTemplate;
}
}
Add it to your shell and mark it as region:
<controls:RegionItemsControl
prism:RegionManager.RegionName="MainRegion">
<controls:RegionItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid />
</ItemsPanelTemplate>
</controls:RegionItemsControl.ItemsPanel>
<controls:RegionItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}"/>
</DataTemplate>
</controls:RegionItemsControl.ItemTemplate>
</controls:RegionItemsControl>
The navigation is handled by an event, which triggers the following code:
private FrameworkElement _lastView = null;
private bool LoadAndActivateWorkspaceAreaView(Type requestedViewType, IRegion region)
{
var viewToActivate = region.Views.FirstOrDefault(viewItem => viewItem.GetType() == requestedViewType) as FrameworkElement;
if (viewToActivate == null)
{
viewToActivate = MefContainer.GetExportedValue(requestedViewType) as FrameworkElement;
if (viewToActivate == null)
throw new InvalidOperationException("view not found!");
viewToActivate.Visibility = Visibility.Collapsed;
region.Add(viewToActivate); // Adds new view to RegionItemsControl
}
if (_lastView != null)
_lastView.Visibility = Visibility.Collapsed;
_lastView = viewToActivate;
_lastView.Visibility = Visibility.Visible;
}
One issue of this solution is, that bindings of "old" views which are stored in RegionItemsControl are re-evaluated once on adding a new view to the region. This seems to be an issue of the ItemsControl I think.
I have a ListView control with support for incremental loading of large item collections. The loading of additional items is triggered when the user scrolls down to the bottom ListView's vertical scrollbar. I strive to use MVVM pattern in the implementation (no code-behind), but IMHO in this particular case it's not essential.
I am tinking about introducing a new event (e.g. NeedsMoreItems) and perhaps subclass ListView (ListViewWithIncrementalLoading) so the application won't need to do ScrollChanged events mapping. I haven't implemented custom events for XAML controls before, so I am not quite sure what's the easiest way to achive this. Do I need to create custom control? User control? Use attached events?
If you have code snippets from similar tasks, I will really appreciate it.
This is not the complete code because this was all very hard to develop and it's very much code (used in the software CCFinder), also it's not perfect, but it does it's job.
<CCFinder:AnimatedScrollViewer VerticalScrollBarVisibility="{Binding IsItemsFound, Converter={StaticResource __boolToVisibilityConverter}}" Margin="36,211,38,72"
ScrollChanged="ScrollViewer_ScrollChanged" HorizontalContentAlignment="Center" Focusable="False" x:Name="ScrolView1">
<ItemsControl Name="_itemsControl" ItemsSource="{Binding CurrentImages}"
...
In the codebehind:
private void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.VerticalOffset + e.ViewportHeight == e.ExtentHeight)
{
var scroller = ((ScrollViewer)sender);
scroller.ReleaseMouseCapture();
scroller.InvalidateScrollInfo();
((OverviewViewModel)this.DataContext).ShowMoreTriggered();
}
}
So here comes the important part: in the ViewModel class I set the MaximumImages property to a higher value... it begins with about 50 and is then raised to 251 and then to 500 when this is triggered again. In the setter of the MaximumImages, the WPF framework is notified that CurrentImages has changed, and in the CurrentImages getter I have a lot of clunky code that realizes that the MaximumImages number is now higher than before, and adds new items to CurrentImages (all in the getter). Not too nice, but it works and is mostly in the ViewModel class:
public void ShowMoreTriggered()
{
if (Photos != null && !ShowMoreTriggeredActive && MaximumImages < Photos.Count)
{
ShowMoreTriggeredActive = true;
ThreadPool.QueueUserWorkItem(delegate
{
Thread.Sleep(1000);
MaximumImages = MaximumImages < 251 ? 251 : 500;
Thread.Sleep(1500);
ShowMoreTriggeredActive = false;
});
}
}
private int _maximumImages;
public int MaximumImages
{
get
{
return _maximumImages;
}
set
{
_maximumImages = value;
InvokePropertyChanged("MaximumImages");
InvokePropertyChanged("CurrentImages");
}
}
Of course it would be more elegant to raise a user-defined event in case the scroll event reaches the lowest point, but I guess it would only produce more code, and not less, because it would certainly would be propagated from the ScrollChanged event anyways.
Consider the following code:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<Slider ValueChanged="slider_ValueChanged/>
<TextBox x:Name="counter"/>
</StackPanel>
</Window>
and
namespace Project1
{
public partial class Window1 : Window
{
public MainWindow() { InitializeComponent(); }
void slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
counter.Text = e.NewValue.ToString();
}
}
}
Slider will raise its ValueChanged event during initialization while counter is still null.
This is an example of a larger problem that I've been running into using WPF, that UI events can fire at any time, and that there is no single place where I can put my initialization code so that it's guaranteed to run after all the pointers owned by the WPF system have been initialized but before any UI events have fired.
What is the most elegant way to deal with this? The fact that this specific example should use data binding is beside the point.
There are many ways to deal with this, depending on your situation
First off, you could simply recognize the fact that the object might not be initialized and check for that before processing. For example,
if (counter.Text != null)
counter.Text = e.NewValue.ToString();
Second, you could attach your events in the Loaded event of the object so they don't fire until after the object has been initialized.
void Counter_Loaded(object sender, EventArgs e)
{
slider.ValueChanged += Slider_ValueChanged;
}
void Counter_Unloaded(object sender, EventArgs e)
{
slider.ValueChanged -= Slider_ValueChanged;
}
And last of all, you can use WPF's Dispatcher to run events on the UI thread at a different DispatcherPriority. The default is Normal, which runs after Loaded, Render, and DataBind operations
Dispatcher.BeginInvoke(DispatcherPriority.DataBind,
new Action(delegate() { counter.Text = e.NewValue.ToString(); }));
The true answer to this question is to use the MVVM pattern where window code behind files contain little to no initialization code.
In this pattern, the UI is connected to the rest of the code with data binding only. You write special view-model classes that implement INotifyPropertyChanged and take your business logic and expose it as a series of properties that UI binds to.
Naturally, you fully control how your view-models initialize.
What would be the best way to save the window position and size in a WPF app?
Currently, I'm saving the window size and position of a WPF App. Here are the events I handle:
SourceInitialized : The saved info is loaded on to the window
WindowClosing : The current info is saved to the backing store
(I copied this from an example).
The problem is, when the window is minimized and restored, the settings from the last WindowClosing is retrieved.
Now, the StateChanged event fire AFTER the window has minimized, so it does not seem to be what i need.
Thanks
Do yourself and your users a favor and use the LocationChanged event and the SizeChanged event to save the settings at that time. There's nothing more annoying than an application that gets amnesia if the process exits abnormally and settings don't get saved (cough...explorer...cough...)
Then just check to make sure the WindowState == Normal before saving the settings. Obviously its pointless to save the position of a minimized or maximized window.
As for when to load the settings, well that you can just do in the constructor after the InitializeComponent call or you can use the Initialized event. No real reason to use the SourceInitialized event unless you are doing something with the HWND directly which shouldn't be necessary.
Use the WindowInteropHelper object to get the window handle and use Screen.FromHandle method to get the actual screen the window is on. When saving make sure to also save the screen bounds just in case it does not exist any more.
One caveat when restoring the screen to its former state is it has to be done after the window handle is created so can't do it in the constructor else won't work properly in multiple monitor situations. Try doing it on the SourceInitialized callback
Are you doing this via databinding? That is the way I do my window sizing and position. I typically have a UserConfig.xml file that is saved in the Users Profile. Then I create elements in there as I databind them in the program. I have the Application.xaml resource dictionary refer to that file, and all of the settings I want set to XPaths inf the XML. Then I just save the in memory xml document on exit. Only one event to handle, no mess, no fuss.
And you can expand it to encompass as many settings as you like in regard to the UI. Adding plumbing settings is a little more difficult, but not terribly so.
I have a solution for saving Size and State, you can extend it to also save the Position. It's done using a Behavior. Simply Binding the Width and Height did not work as expected, because it would overwrite the "Normal" state's size with the maximized sizes. That's why there are some extra checks like if(state == normal)
There is a Config Property on my Window's DataContext.
You'll need a reference to System.Windows.Interactivity to do that.
public class MainWindowSaveStateBehavior : Behavior<Window>
{
public Config Config
{
get { return (Config)GetValue(ConfigProperty); }
set { SetValue(ConfigProperty, value); }
}
public static readonly DependencyProperty ConfigProperty =
DependencyProperty.Register("Config", typeof(Config), typeof(MainWindowSaveStateBehavior), new PropertyMetadata(Config_Changed));
private static void Config_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var b = d as MainWindowSaveStateBehavior;
if(e.NewValue != null) b.LoadSettings();
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.SizeChanged += Window_SizeChanged;
AssociatedObject.StateChanged += Window_StateChanged;
LoadSettings();
}
bool _initialized = false;
private void Window_StateChanged(object sender, EventArgs e)
{
SaveSettings();
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
SaveSettings();
}
private void LoadSettings()
{
if (Config == null) return;
AssociatedObject.Width = Config.WindowWidth;
AssociatedObject.Height = Config.WindowHeight;
AssociatedObject.WindowState = Config.WindowState;
_initialized = true;
}
private void SaveSettings()
{
if (Config == null || !_initialized) return;
Config.WindowState = AssociatedObject.WindowState;
if(AssociatedObject.WindowState == WindowState.Normal)
{
Config.WindowWidth = AssociatedObject.Width;
Config.WindowHeight = AssociatedObject.Height;
}
}
}
In Xaml use the behavior by adding the namespaces
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:b="<the namespace your behavior lives in>"
And then attach the Behavior
<i:Interaction.Behaviors>
<b:MainWindowSaveStateBehavior Config="{Binding Config}" />
</i:Interaction.Behaviors>
You then just have to Load and Save the Config in your DataContext on startup/shutdown.
I liked CodeWarriors answer. I use TwoWay binding to apps settings:
Height="{Binding Source={x:Static p:Settings.Default}, Path=WindowHeight, Mode=TwoWay}"
Width="{Binding Source={x:Static p:Settings.Default}, Path=WindowWidth, Mode=TwoWay}"
Top="{Binding Source={x:Static p:Settings.Default}, Path=WindowTop, Mode=TwoWay}"
Left="{Binding Source={x:Static p:Settings.Default}, Path=WindowLeft, Mode=TwoWay}"
where p - project's properties namespace.
i'm just starting with the mvvm model in Silverlight.
In step 1 i got a listbox bound to my viewmodel, but now i want to propagate a click in a button and a selecteditemchanged of the listbox back to the viewmodel.
I guess i have to bind the click event of the button and the selecteditemchanged of the listbox to 2 methods in my viewmodel somehow?
For the selecteditemchanged of the listbox i think there must also be a 'return call' possible when the viewmodel tries to set the selecteditem to another value?
i come from a asp.net (mvc) background, but can't figure out how to do it in silverlight.
Roboblob provides excellent step-by-step solution for Silverlight 4. It strictly follows MVVM paradigm.
I would not bind or tie the VM in any way directly to the events of controls within the View. Instead, have a separate event that is raised by the View in response to the button click.
[disclaimer: this code is all done straight from my head, not copy & pasted from VS - treat it as an example!!]
So in pseudo code, the View will look like this:
private void MyView_Loaded(...)
{
MyButton.Click += new EventHandler(MyButton_Click);
}
private void MyButton_Click(...)
{
//Raise my event:
OnUserPressedGo();
}
private void OnUserPressedGo()
{
if (UserPressedTheGoButton != null)
this.UserPressedTheGoButton(this, EventArgs.Empty);
}
public EventHandler UserPressedTheGoButton;
and the VM would have a line like this:
MyView.UserPressedTheGoButton += new EventHandler(myHandler);
this may seem a little long-winded, why not do it a bit more directly? The main reason for this is you do not want to tie your VM too tightly (if at all) to the contents of the View, otherwise it becomes difficult to change the View. Having one UI agnostic event like this means the button can change at any time without affecting the VM - you could change it from a button to a hyperlink or that kool kat designer you hire may change it to something totally weird and funky, it doesn't matter.
Now, let's talk about the SelectedItemChanged event of the listbox. Chances are you want to intercept an event for this so that you can modify the data bound to another control in the View. If this is a correct assumption, then read on - if i'm wrong then stop reading and reuse the example from above :)
The odds are that you may be able to get away with not needing a handler for that event. If you bind the SelectedItem of the listbox to a property in the VM:
<ListBox ItemSource={Binding SomeList} SelectedItem={Binding MyListSelectedItem} />
and then in the MyListSelectedItem property of the VM:
public object MyListSelectedItem
{
get { return _myListSelectedItem; }
set
{
bool changed = _myListSelectedItem != value;
if (changed)
{
_myListSelectedItem = value;
OnPropertyChanged("MyListSelectedItem");
}
}
}
private void OnPropertyChanged(string propertyName)
{
if (this.NotifyPropertyChanged != null)
this.NotifyPropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
To get that NotifyPropertyChanged event, just implement the INotifyPropertyChanged interface on your VM (which you should have done already). That is the basic stuff out of the way... what you then follow this up with is a NotifyPropertyChanged event handler on the VM itself:
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "MyListSelectedItem":
//at this point i know the value of MyListSelectedItem has changed, so
//i can now retrieve its value and use it to modify a secondary
//piece of data:
MySecondaryList = AllAvailableItemsForSecondaryList.Select(p => p.Id == MyListSelectedItem.Id);
break;
}
}
All you need now is for MySecondaryList to also notify that its value has changed:
public List<someObject> MySecondaryList
{
get { return _mySecondaryList; }
set
{
bool changed = .......;
if (changed)
{
... etc ...
OnNotifyPropertyChanged("MySecondaryList");
}
}
}
and anything bound to it will automatically be updated. Once again, it may seem that this is the long way to do things, but it means you have avoided having any handlers for UI events from the View, you have kept the abstraction between the View and the ViewModel.
I hope this has made some sense to you. With my code, i try to have the ViewModel knowing absolutely zero about the View, and the View only knowing the bare minimum about the ViewModel (the View recieves the ViewModel as an interface, so it can only know what the interface has specified).
Regarding binding the button click event I can recommend Laurent Bugnion's MVVM Light Toolkit (http://www.galasoft.ch/mvvm/getstarted/) as a way of dealing with this, I'll provide a little example, but Laurent's documentation is most likely a better way of understanding his framework.
Reference a couple of assemblies in your xaml page
xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
add a blend behaviour to the button
<Button Content="Press Me">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<command:EventToCommand Command="{Binding ViewModelEventName}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
and create the event within your viewmodel which will be called when the button is clicked
public RelayCommand ViewModelEventName { get; protected set; }
...
public PageViewModel()
{
ViewModelEventName = new RelayCommand(
() => DoWork()
);
}
This supports passing parameters, checking whether execution is allowed etc also.
Although I haven't used it myself, I think the Prism framework also allows you to do something similar.