I have a tab control where the tabs are created at runtime. The contents of the tabs will be one of several user controls, each containing hundreds of other controls. As it takes a long time to create these user controls, I'm trying to find a way to reuse them rather than creating a new instance for each tab.
I'm setting the tab page contents using a DataTemplate as follows:
<DataTemplate>
<ScrollViewer Content="{Binding Content}" />
</DataTemplate>
Where Content is the view model for the view I want to show in the tab.
Elsewhere I use data templates to map each view model to a view, e.g.
<DataTemplate DataType="{x:Type vm:MyViewModel1}">
<ctl:CacheContentControl ContentType="{x:Type ctl:MyView1}" />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:MyViewModel2}">
<ctl:CacheContentControl ContentType="{x:Type ctl:MyView2}" />
</DataTemplate>
CacheContentControl is a wrapper around ContentControl that I use for the caching:
public class CacheContentControl : ContentControl
{
private static Dictionary<Type, Control> cache = new Dictionary<Type, Control>();
public CacheContentControl()
{
Unloaded += CacheContentControl_Unloaded;
}
private void CacheContentControl_Unloaded(object sender, RoutedEventArgs e)
{
Content = null;
}
private Type _contentType;
public Type ContentType
{
get { return _contentType; }
set
{
_contentType = value;
Content = GetView(_contentType);
}
}
public Control GetView(Type type)
{
if (!cache.ContainsKey(type))
{
cache.Add(type, (Control)Activator.CreateInstance(type));
}
return cache[type];
}
}
This ensures that the DataTemplate first checks the cache to see if it can reuse a control before creating a new instance.
This works for any new tabs that are created. The first tab is slow as expected since it needs to create the initial control, but all subsequent tabs using the same control load almost immediately. The problem I'm having is that when I click back to a previous tab the control no longer appears and the tab is blank.
I guess this is happening because I can't show the same control instance on the same form multiple times which makes sense. I've been able to work around it by handing the IsVisibleChanged event for my CacheContentControl as follows:
private void CacheContentControl_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
if (IsVisible && ContentType != null)
{
Control ctl = GetView(_contentType);
ctl.DataContext = DataContext;
Content = ctl;
}
else
{
Content = null;
}
}
When a tab loses focus it removes the control, the tab that receives focus can then retrieve the control from the cache which seems to work.
The issue is that the speeds have gone back to being slow again and I'm not sure why. Obviously it's possible to move a control instance from one tab to another without delay as it does this every time I create a new tab. There must be something the DataTemplate does differently to change the control's parent?
I’m not sure that you need to go so far as to cache your user controls as WPF largely does this for you.
The normal approach inside a DataTemplate keyed by type is not to use a ContentControl but simply to have the UserControl as the template.
<DataTemplate x:Key=“MyViewmodelType”>
<MyViewControl />
</DataTemplate>
The TabControl will have a ContentControl which has its Content set to whichever ViewModel is applicable and WPF will automatically render it using the specified DataTemplate for that type and set the DataContext.
I have built massive WPF Views that were rendered using DataTemplates as per the simpler approach outlined above without encountering any serious performance issues and I think that you could safely start developing by simply binding ContentControl.Content to your ViewModel instance and letting WPF handle the rendering using the simpler DataTemplate approach above.
Related
Im new to MVVM and try to follow all the guidelines I find to respect it. I would like to have a Busy-Animation on one of my usercontrols. I want to include it on the control like this.
The Usercontrol it is nested in is shown on the MainWindow using a DataTemplate for a ViewModel, for example like so:
<Window.Resources>
<DataTemplate DataType="{x:Type AppViews:AppConfigViewModel}">
<local:AppConfigView />
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content="{Binding CurrentPageViewModel}" />
</Grid>
When running this, the Application is shown and I also see the view for the AppConfigViewModel which is bind correctly since underlying values are displayed correctly in the view.
Now I tried to register the Busy-Animation in the ViewModel (to control it from there) by doing this in the Constructor of the BusyAnimation:
(DataContext as PageViewModel).BusyAnim = this;
For some reason the DataContext is always null and the result of this line is an exception. What am I doing wrong here?
What I tried to did there is against the idea of MVVM.
I tried downcasting an object that is meant to be general.
A better aproach for the task I tried to achieve is implementing dependency properties in the busy animation component. Those are meant to be bound to from the viewmodel of the mainly displayed view. that way the busy animation can be shown when some property in the viewmodel changes. That could be for example a bool with the name "working".
this is the dependency property code in my busy animation:
public static readonly DependencyProperty ShowBusyProperty = DependencyProperty.Register("ShowBusy", typeof(Boolean), typeof(FortschrittView), new PropertyMetadata(false, OnShowBusyPropertyChanged));
public Boolean ShowBusy
{
get { return (Boolean)this.GetValue(ShowBusyProperty); }
set { this.SetValue(ShowBusyProperty, value); }
}
private static void OnShowBusyPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
FortschrittView myUserControl = dependencyObject as FortschrittView;
myUserControl.OnPropertyChanged("ShowBusy");
myUserControl.OnShowBusyPropertyChanged(e);
}
private void OnShowBusyPropertyChanged(DependencyPropertyChangedEventArgs e)
{
if(ShowBusy)
{
Start();
}
else
{
Stop();
}
}
Yes its a lot of code, but I feel wpf wants it that way. Remember above code is in the busy-animation user control and triggers Start() Stop() functions which control storyboards.
Below xaml is in the control that uses the busyanimation, binding it to a viewmodel that the busy-animation should indicate background-work for:
<local:BusyAnimation ShowBusy="{Binding Model.IsBusy}"/>
That ShowBusy Property there is the Dependency Property implemented above. Of course IsBusy from the model should follow the observable pattern for everything to work.
/ps: I throughoutly documented the mistakes i did and how i solved them. Can I get rid of the negative points I got somehow for creating this question?
I have a wpf Custom Control on which I have been working. It has a shared New like this:
Shared Sub New()
'This OverrideMetadata call tells the system that this element wants to provide a style that is different than its base class.
'This style is defined in themes\generic.xaml
DefaultStyleKeyProperty.OverrideMetadata(GetType(VtlDataNavigator_24), New FrameworkPropertyMetadata(GetType(VtlDataNavigator_24)))
ItemsSourceProperty.OverrideMetadata(GetType(VtlDataNavigator_24), New FrameworkPropertyMetadata(Nothing, AddressOf OnItemsSourceHasChanged))
End Sub
If an Items source has been set for the custom control this shared sub then invokes the overrideMetadata for the itemssource (as shown below)
Private Shared Sub OnItemsSourceHasChanged(ByVal d As DependencyObject, ByVal baseValue As Object)
Dim vdn As VtlDataNavigator_24 = DirectCast(d, VtlDataNavigator_24)
vdn.RecordCount = vdn.Items.SourceCollection.Cast(Of Object)().Count()
vdn.MyBaseCollection = DirectCast(vdn.ItemsSource, ICollectionView)
vdn.MyBaseEditableCollection = DirectCast(vdn.ItemsSource, IEditableCollectionView)
vdn.MyBaseCollection.MoveCurrentToFirst
vdn.RecordIndex = vdn.MyBaseCollection.CurrentPosition + 1
If Not IsNothing(vdn.FindButton) Then
If vdn.FindButton.Visibility = Visibility.Visible Then
vdn.RecordIndexTextBox.IsReadOnly = False
Else
vdn.RecordIndexTextBox.IsReadOnly = True
End If
End If
vdn.ResetTheNavigationButtons
vdn.SetupInitialStatesForNonNavigationButtons
End Sub
This then fails because buttons referred to in the code (and routines called from it) have not yet been instantiated because the override for OnApplyTemplate (shown below) has not been called.
Public Overrides Sub OnApplyTemplate()
MyBase.OnApplyTemplate()
RecordIndexTextBox = CType(GetTemplateChild("PART_RecordIndexTextBox"), TextBox)
RecordCountTextBox = CType(GetTemplateChild(RecordCountTextBoxPart), TextBox)
RecordTextBlock = CType(GetTemplateChild(RecordTextBlockPart), TextBlock)
OfTextBlock = CType(GetTemplateChild(OfTextBlockPart), TextBlock)
FirstButton = CType(GetTemplateChild(FirstButtonPart), Button)
PreviousButton = CType(GetTemplateChild(PreviousButtonPart), RepeatButton)
NextButton = CType(GetTemplateChild(NextButtonPart), RepeatButton)
LastButton = CType(GetTemplateChild(LastButtonPart), Button)
AddButton = CType(GetTemplateChild(AddButtonPart), Button)
CancelNewRecordButton = CType(GetTemplateChild(CancelNewButtonPart), Button)
EditButton = CType(GetTemplateChild(EditButtonPart), button)
CancelButton = CType(GetTemplateChild(CancelButtonPart), Button)
RefreshButton = CType(GetTemplateChild(RefreshButtonPart), Button)
SaveButton = CType(GetTemplateChild(SaveButtonPart), Button)
DeleteButton = CType(GetTemplateChild(DeleteButtonPart), Button)
FindButton = CType(GetTemplateChild(FindButtonPart), Button)
End Sub
If I add something along the lines of:
vdn.OnApplyTemplate
to OnItemsSourceHasChanged, OnApplyTemplate is called but nothing is resolved (see illustration below).
BUT if I don't set an itemssource on my control, then OnApplyTemplate gets called and the items resolve (see below)
Has anyone encountered this sort of behaviour before and found a way to correct it such that OnApplyTemplate is always the first thing to get called before anything that might require access to controls that have yet to be resolved.
Edit
The curious thing about this issue is that (and doesn't this always seem to be the case!) this was working until obviously I did something or set some property. What I am left with is a project that runs if I do not set an Items source on my custom control, and one which doesn't if I do because the custom handler I have in place to handle when the items source is changed on my custom control is running before OnApplyTemplate gets called.
Well I have at last been able to determine that my custom controls Itemssource property is being changed before the control is being drawn and rendered and therefore the code I have in place to set things up following the ItemsSource change raises null reference exceptions because the main control has yet to be rendered.
Given that it did work it must be something I've done but I'm now out od ideas as to how to delve into this further and actually find the reason. I'd welcome any suggestions you might have or potential work rounds.
Edit in relation to comments below: typical part of control template.
<!-- First Button -->
<Button Style="{StaticResource vtlNavButtonStyle}"
x:Name="PART_FirstButton"
Tag="First_Button"
Visibility="{Binding Path=NavigationButtonVisibility,Converter={StaticResource booltovis}, RelativeSource={RelativeSource TemplatedParent}}"
ToolTipService.ShowOnDisabled="False"
ToolTipService.ShowDuration="3000"
ToolTipService.InitialShowDelay="500">
<Button.ToolTip>
<Binding Path="FirstButtonToolTip"
RelativeSource="{RelativeSource TemplatedParent}"
TargetNullValue="{x:Static p:Resources.FirstText}">
</Binding>
</Button.ToolTip>
<StackPanel>
<Image Style="{StaticResource vtlImageStyle}">
<Image.Source>
<Binding Path="FirstImage"
RelativeSource="{RelativeSource TemplatedParent}">
<Binding.TargetNullValue>
<ImageSource>/VtlWpfControls;component/Images/16/first.png</ImageSource>
</Binding.TargetNullValue>
</Binding>
</Image.Source>
</Image>
</StackPanel>
</Button>
Calling OnApplyTemplate yourself isn't going to help; the framework will call it when the template has actually been applied. That said, the order in which things happen is not deterministic -- the template may or may not be applied before the ItemsSource is set. I'm working with UWP apps for Windows 10, which is a slightly different beast, but we've solved a similar issue doing something like this:
private TextBlock textBlock;
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Grab the template controls, e.g.:
textBlock = GetTemplateChild("MyTextBlock") as TextBlock;
InitializeDataContext();
DataContextChanged += (sender, args) => InitializeDataContext();
}
private void InitializeDataContext()
{
ViewModel ViewModel = DataContext as ViewModel;
if (viewModel != null)
{
// Here we know that both conditions are satisfied
textBlock.Text = ViewModel.Name;
}
}
The key is to not start listening for DataContextChanged until the template has been applied. If the data context has already been set, the first call to initializeDataContext takes care of things; if not, the callback takes care of things.
(In your case, replace our data context listening with items source listening, I suppose.)
This isn't an answer to your question, but instead expands on some things you mentioned in the comments.
I really think that it would benefit you to look into WPF commands as they pertain to custom controls. Your data navigator control sounds like it essentially supports a number of actions (go to first/previous/next/last; add; edit; cancel; etc) that you invoke using Button controls in the control template. Rather than looking for the buttons in OnApplyTemplate (at which point you store references to them so that you can presumably hook into their Click event later) you should support commands in your control: the buttons in the template would then bind to these commands.
An example would probably make this a bit clearer. The following is code for a custom control that supports two actions: go-to-first-page, and go-to-last-page. In the static constructor I register two command bindings, one for each action. These work by calling into a helper method that takes the command to "bind" to, plus a pair of delegates that get called when the action is invoked.
The commands I am using here are provided by the WPF framework, and are static properties contained in the static NavigationCommands class. (There are a bunch of other similar classes containing commands, just follow the links in the "See Also" section of that MSDN page).
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace StackOverflow
{
public class TestControl : Control
{
static TestControl()
{
RegisterCommandBinding<TestControl>(NavigationCommands.FirstPage,
x => x.GoToFirstPage());
RegisterCommandBinding<TestControl>(NavigationCommands.LastPage,
x => x.GoToLastPage(), x => x.CanGoToLastPage());
DefaultStyleKeyProperty.OverrideMetadata(typeof(TestControl),
new FrameworkPropertyMetadata(typeof(TestControl)));
}
void GoToFirstPage()
{
Console.WriteLine("first page");
}
void GoToLastPage()
{
Console.WriteLine("last page");
}
bool CanGoToLastPage()
{
return true; // Would put your own logic here obviously
}
public static void RegisterCommandBinding<TControl>(
ICommand command, Action<TControl> execute) where TControl : class
{
RegisterCommandBinding<TControl>(command, execute, target => true);
}
public static void RegisterCommandBinding<TControl>(
ICommand command, Action<TControl> execute, Func<TControl, bool> canExecute)
where TControl : class
{
var commandBinding = new CommandBinding(command,
(target, e) => execute((TControl) target),
(target, e) => e.CanExecute = canExecute((TControl) target));
CommandManager.RegisterClassCommandBinding(typeof(TControl), commandBinding);
}
}
}
The following is the control's default template. As you can see there are simply two Button controls, each one of which binds to the relevant command via its Command property (note this is not a data binding, ie. you're not using the {Binding} markup extension).
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverflow">
<Style TargetType="{x:Type local:TestControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TestControl}">
<StackPanel Orientation="Horizontal">
<Button Command="NavigationCommands.FirstPage" Content="First" />
<Button Command="NavigationCommands.LastPage" Content="Last" />
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Finally, here's the custom control in a Window. As you click the "First" and "Last" buttons you can see the actions being invoked by watching the relevant text appear in the debug console window.
<Window x:Class="StackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StackOverflow">
<local:TestControl VerticalAlignment="Top" />
</Window>
If you use commands in this way then you should be able to simplify your control's code significantly.
I had a similar issue - a custom control (specifically, a class derived from Control) would show binding errors whenever a new instance of the control was instantiated. This was because the control template was being created before the bindings were setup. Once the bindings took effect, then the control would start to work.
To "fix" this (or work around it anyway) I just added a call to ApplyTemplate() to the control's constructor. So it ends up looking like this:
public CustomControl()
{
InitializeComponent();
ApplyTemplate();
}
Then there were no more binding errors.
In my WPF app, I've to show different controls in the window for different user actions. Like, when user selects an image, I show a Image control and when the user selects a text file, I show a TextBox control. Similarly there are many controls for different user selections.
To do this I'm first doing Visibility = Visibility.Collapsed for all controls using a foreach loop, then doing Visibility = Visibility.Visible for the controls that I've to show.
Is there a more efficient way of doing this? The window flickers and is not really snappy when changing controls as there are many controls.
I always use a ContentControl for this. It produces very clean xaml code and is superfast, I've never seen any flicker even in rather loaded views. It does require any extra frameworks beyond WPF.
<ContentControl Content="{Binding Selected}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type viewModels:FirstOne}">
<!-- View code for first view goes here -->
<TextBlock>Hi</TextBlock>
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:SecondOne}">
<!-- View code for second view goes here -->
<Image Source="{Binding Image}" />
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
And then I have a MainViewModel handing the navigation with a property
class MainViewModel: ViewModel, INavigation
{
public ViewModel Selected
{
get { return _selected; }
private set
{
_selected = value;
RaisePropertyChanged();
}
public void Show(ViewModel viewModel) { Selected = viewModel; }
}
}
The INavigation interface is just something like so
interface INavigation { void Show(ViewModel viewModel); }
An example view model
class FirstOne: ViewModel
{
private readonly INavigation _navigation;
public FirstOne(INavigation navigation) { _navigation = navigation; }
public void ButtonClicked()
{
_navigation.Show(new SecondOne());
}
}
First, you may want to avoid doing Visibility=Visibility.Collapsed for controls that you are about to make visible. That could reduce some unnecessary screen activity.
Second, rather than having the user experience all the individual visibility changes in a serial fashion, you might want to keep your controls in a parent control that you can hide before the visibility changes take place and then unhide once the changes are complete. You could even use an opacity animation to fade it out and then back in. Of course, without seeing or understanding more about your app, I can't tell what's really appropriate.
I have a UserControl named "WorkspaceView" and its only purpose is to show other views as tabs. Call these views ViewA, ViewB etc. Which of these views to present should be determined on runtime, so I figured I needed a control that can present ... well ..stuff.
ContentControl to the rescue. Except ... I can't make it work. I'm trying to new up a usercontrol of type ViewA in the code behind and assign it to my MyContent, which is the ContentControl. I've tried:
public WorkspaceView()
{
InitializeComponent();
DataContext = new View(A); //Hoping that the DataContext will propagate down
}
Second attempt was
public WorkspaceView()
{
InitializeComponent();
var binding = new Binding {Source = new ViewA()};
MyContent.SetBinding(ContentControl.ContentProperty, binding);
}
In both cases, I see an empty box, but since I've hard wired a TextBlock into ViewA, I'd expect it to show me that text. What am I doing wrong?
Despite knowing that MVVM is the preferred way to develop WPF applications, I'd prefer to see how I can do this with code behind files. Later on, I will redo the application with MVVM, but first I need to get some basic understanding of WPF.
In response to the suggestions so far, I've tried
MyContent.Content = new ViewA();
but still I the text that is in ViewA does not appear. I've also at the bottom of this post included a screenshot of what the application renders.
WorkspaceView
Resource file
What is rendered
Have you tried simply doing this?
MyContent.Content = new ViewA();
EDIT
Try simplifying your code a bit and working from there. For instance:
public WorkspaceView()
{
InitializeComponent();
// Something better than UserControl should be used here
ObservableCollection<UserControl> views = new ObservableCollection<UserControl>();
views.Add(new ViewA());
views.Add(new ViewB());
DataContext = views;
}
<Border ..>
<TabControl x:Name="TabControl"
..
ItemsSource="{Binding}" />
</Border>
This code sets a WorkspaceView.DataContext to a collection of UserControls. When you specify {Binding} whithin WorkspaceView's XAML you are refering to the whole DataContext object (i.e. your collection.) This way you are setting the TabControl.ItemsSource to your collection of views.
Now you could create DataTemplates targeting the type of each view to control how each control is displayed in its tab within the TabControl.
I've created a simple WPF application which has two Windows. The user fills in some information on the first Window and then clicks Ok which will take them to the second Window. This is working fine but I'm trying to incorporate both Windows into a single Window so just the content changes.
I managed to find this Resource management when changing window content which seems like it is what I'm after. However, I've search for ContentPresenter but couldn't find much help for how I need to use it. For example, if I use a ContentPresenter, where do I put the existing XAML elements that are in the two Windows? I'm guessing the first Window will go into the ContentPresenter but the second one will need to be put somewhere for when it needs to be switched in.
Any help would be great. A simple working example would be even better.
TIA
A ContentPresenter is normally used when restyling existing controls. It is the place where the Content of a control is placed. Instead you should use a ContentControl, which is simply a control that has a content element. Alternatively, you could directly set the Content of your window.
You extract the contents of your two existing windows into two UserControls. Then you create a new Window which will host the contents. Depending on your business logic, you set the content of that window (or that window's ContentControl if you want additional "master" content) to either of those two UserControls.
EDIT:
As a starting point. This is not complete working code, just to get you started. Note that this is bad architecture; you should probably use a MVVM or similar approach once you get this running!
<Window>
<ContentControl Name="ContentHolder" />
</Window>
<UserControl x:Class="MyFirstUserControl" /> <!-- Originally the first window -->
<UserControl x:Class="MySecondUserControl" /> <!-- Originally the second window -->
In code behind of Window:
// Somewhere, ex. in constructor
this.ContentHolder.Content = new MyFirstUserControl;
// Somewhere else, ex. in reaction to user interaction
this.ContentHolder.Content = new MySecondUserControl;
I use ContentPresenter for snapping in content. In the window, I put something like this:
<ContentPresenter Content="{Binding MainContent}" />
In the view model, I have a property called MainContent of type object:
public object MainContent { get { return (object)GetValue(MainContentProperty); } set { SetValue(MainContentProperty, value); } }
public static readonly DependencyProperty MainContentProperty = DependencyProperty.Register("MainContent", typeof(object), typeof(SomeViewModel), new FrameworkPropertyMetadata(null));
Whatever you set MainContent to will show up in the window.
To keep the separation between view and view model, I typically set the MainContent property to another view model and use a data template to map that view model to a view:
<DataTemplate DataType="{x:Type viewmodels:PlanViewModel}">
<views:PlanView />
</DataTemplate>
I put that data template in some central resource dictionary along with a bunch of other view-model-to-view mappers.