Showing different controls programmatically - c#

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.

Related

Reusing tab content with DataTemplate

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.

WPF MVVM Exception when Registering UserControl in DataContext

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?

Is there a way of using virtualization with hidden panels or expanders?

I'm trying to improve performance with my WPF application and I'm having problems with a complex ItemsControl. Although I've added Virtualization, there's still a performance problem and I think I've worked out why.
Each item contains a series of expandable areas. So the user sees a summary at the start but can drill down by expanding to see more information. Here's how it looks:
As you can see, there's some nested ItemsControls. So each of the top level items has a bunch of Hidden controls. The virtualization prevents off-screen items from loading, but not the hidden items within the items themselves. As a result, the relatively simple initial layout takes a significant time. Flicking around some of these views, 87% of time is spent parsing and Layout, and it takes a few seconds to load.
I'd much rather have it take 200ms to expand when (if!) the user decides to, rather than 2s to load the page as a whole.
Asking for advice really. I can't think of a nice way of adding the controls using MVVM however. Is there any expander, or visibility based virtualization supported in WPF or would I be creating my own implementation?
The 87% figure comes from the diagnostics:
If you simply have
- Expander
Container
some bindings
- Expander
Container
some bindings
+ Expander
+ Expander
... invisible items
Then yes, Container and all bindings are initialized at the moment when view is displayed (and ItemsControl creates ContentPresenter for visible items).
If you want to virtualize content of Expander when it's collapsed, then you can use data-templating
public ObservableCollection<Item> Items = ... // bind ItemsControl.ItemsSource to this
class Item : INotifyPropertyChanged
{
bool _isExpanded;
public bool IsExpanded // bind Expander.IsExpanded to this
{
get { return _isExpanded; }
set
{
Data = value ? new SubItem(this) : null;
OnPropertyChanged(nameof(Data));
}
}
public object Data {get; private set;} // bind item Content to this
}
public SubItem: INotifyPropertyChanged { ... }
I hope there is no need to explain how to to do data-templating of SubItem in xaml.
If you do that then initially Data == null and nothing except Expander is loaded. As soon as it's expanded (by user or programmatically) view will create visuals.
I thought I'd put the details of the solution, which is pretty much a direct implementation of Sinatr's answer.
I used a content control, with a very simple data template selector. The template selector simply checks if the content item is null, and chooses between two data templates:
public class VirtualizationNullTemplateSelector : DataTemplateSelector
{
public DataTemplate NullTemplate { get; set; }
public DataTemplate Template { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item == null)
{
return NullTemplate;
}
else
{
return Template;
}
}
}
The reason for this is that the ContentControl I used still lays out the data template even if the content is null. So I set these two templates in the xaml:
<ContentControl Content="{Binding VirtualizedViewModel}" Grid.Row="1" Grid.ColumnSpan="2" ><!--Visibility="{Binding Expanded}"-->
<ContentControl.Resources>
<DataTemplate x:Key="Template">
<StackPanel>
...complex layout that isn't often seen...
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="NullTemplate"/>
</ContentControl.Resources>
<ContentControl.ContentTemplateSelector>
<Helpers:VirtualizationNullTemplateSelector Template="{StaticResource Template}" NullTemplate="{StaticResource NullTemplate}"/>
</ContentControl.ContentTemplateSelector>
</ContentControl>
Finally, rather than using a whole new class for a sub-item, it's pretty simple to create a "VirtualizedViewModel" object in your view model that references "this":
private bool expanded;
public bool Expanded
{
get { return expanded; }
set
{
if (expanded != value)
{
expanded = value;
NotifyOfPropertyChange(() => VirtualizedViewModel);
NotifyOfPropertyChange(() => Expanded);
}
}
}
public MyViewModel VirtualizedViewModel
{
get
{
if (Expanded)
{
return this;
}
else
{
return null;
}
}
}
I've reduced the 2-3s loading time by about by about 75% and it seems much more reasonable now.
This simple solution helped me:
<Expander x:Name="exp1">
<Expander.Header>
...
</Expander.Header>
<StackPanel
Margin="10,0,0,0"
Visibility="{Binding ElementName=exp1, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
<Expander x:Name="exp2">
<Expander.Header>
...
</Expander.Header>
<StackPanel
Margin="10,0,0,0"
Visibility="{Binding ElementName=exp2, Path=IsExpanded, Converter={StaticResource BooleanToVisibilityConverter}}">
An easier way to achieve this is to change the default Visibility of the contents to Collapsed. In this case WPF won't create it initially, but only when a Trigger sets it to Visible:
<Trigger Property="IsExpanded" Value="true">
<Setter Property="Visibility"Value="Visible" TargetName="ExpandSite"/>
</Trigger>
Here "ExpandSite" is the ContentPresenter within the default ControlTemplate of the Expander control.
Note that this has been fixed in .NET - see the default style from the WPF sources on github.
In case you have an older version, you can still use this fixed control template to update the old one with an implicit style.
You can apply the same technique to any other panel or control.
It's easy to check if the control was already created with Snoop. Once you attached it to your application, you can filter the visual tree with the textbox on the top left. If you don't find one control in the tree, it means it was not created yet.

WPF Caliburn.Micro/mvvm Navigation

I'm building a project, and one of the biggest problems I've come across until now is navigation.
I've been looking for some time now for examples of caliburn.micro/mvvm navigation, but they all seem to be really long and I couldn't really understand much of it (beginner here!).
Some info about my project:
I want there to be an outer window/shell, with menu links/tabs that open pages according to the button clicked inside an inner part of the shell, and be able to open change the page from within a one.
I currently have: ShellViewModel.cs, MainViewModel.cs, my models, and my views.
For now, all I need to know is how to make MainViewModel load inside shellviewmodel on startup(using contentcontrol/frames...), and how to move from one page to another.
You could also just write it in points, and link me to some useful examples, and I believe I could continue from there. It'd be best to get a thorough explanation of stuff if possible.
Have a read about Conductors and Screens on the official documentation.
As a simple example, your ShellViewModel could be a Conductor of one active screen (i.e. only one screen becomes active/inactive at a time):
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
You can then set the ActiveItem of the Conductor to the view model instance that you wish to be currently active:
this.ActivateItem(myMainViewModel);
A collection Conductor type also provides an Items collection which you can populate as you instantiate new windows. Viewmodels in this Items collection may be those that are currently deactivated but not yet closed, and you can activate them by using ActivateItem as above. It also makes it very easy to create a menu of open windows by using an ItemsControl with x:Name="Items" in your ShellView.
Then, to create the ShellView, you can use a ContentControl and set its name to be the same as the ActiveItem property, and Caliburn.Micro will do the rest:
<ContentControl x:Name="ActiveItem" />
You can then respond to activation/deactivation in your MainViewModel by overriding OnActivate/OnDeactivate in that class.
In ShellView you use a content control like this:
<ShellView xmlns:cal="http://caliburnproject.org/">
<StackPanel>
<Button Content="Show other view" cal:Message.Attach="ShowOtherView" />
<ContentControl cal:View.Model="{Binding Child}" />
</StackPanel>
</ShellView>
ShellViewModel:
public class ShellViewModel : Screen
{
private object Child;
public object Child
{
get{ return child; }
set
{
if(child == value)
return;
child = value;
NotifyOfPropertyChange(() => Child);
}
}
public ShellViewModel()
{
this.Child = new MainViewModel();
}
public void ShowOtherView()
{
this.Child = new FooViewModel();
}
}
So this is a very basic example. But as you see, your ShellView provides a ContentControl, which shows the child view. This ContentControl is bound via View.Model to the Child property from your ShellViewModel.
In ShellView, I used a button to show a different view, but you can also use a menu or something like that.

How to set IsReadOnly / IsEnabled on entire container like Panel or GroupBox using XAML?

I have a MVVM application that contains multiple views with some complex IsReadOnly rules based on user permissions, view/edit mode and object state.
I would like to set IsReadOnly and/or IsEnabled properties for entire groups of controls in the same container (GroupBox / StackPanel / Grid / UserControl / etc.). The value of this property will be defined in ViewModel.
I've got 3-6 different SomeGroupIsReadOnly properties per UserControl (with a large number of input controls like TextBox, RadioButtons, ComboBoxes and some DataGrids) and I'm looking for a generic, MVVM-friendly solution, that will allow me to reuse Bindings on per-container basis, instead of specifying them for each individual control separately.
How can I set IsReadOnly / IsEnabled on all controls inside container like Panel or GroupBox using XAML?
It doesn't seem that WPF supports this out of the box...
EDIT
I forgot to mention that setting IsEnabled for a container disables an important feature of TextBoxes - being able to copy their contents. I need them to be in IsReadOnly=true state. If there was a workarond for that, then my problem would be solved.
Disabling the Panel that contains your controls will also disable the controls inside the Panel. Bind the IsEnabled member of the Panel to a bool property on your ViewModel and set it according to your rules to disable the Panel and all of its children.
Xaml:
<GroupBox IsEnabled="{Binding Path=IsGroupEnabled}">
<StackPanel>
<Button Content="Sample Button"/>
<TextBox Text="Sample text box"/>
</StackPanel>
</GroupBox>
Something that's worked well for us is to define a view model that represents the permission structure of your application (YourPermissionsViewModel in my example below).
Then you could create a custom panel control that extends any panel of your choice (StackPanel in this example). That way you can add in the IsReadOnly property bindings and persist them to the panel's children.
Here's what the panel in XAML could look like:
<local:PanelExtension IsEnabled="{Binding YourPermissionsViewModel.IsEnabled}"
IsReadOnly="{Binding YourPermissionsViewModel.IsReadOnly}">
<TextBox Text="eeny" Width="100" />
<TextBox Text="meeny" Width="100"/>
<TextBox Text="miny" Width="100"/>
<TextBox Text="mo" Width="100" />
<Label Content="coolio" Width="100" />
</local:PanelExtension>
And here is the StackPanel extension control containing all of StackPanel's features as well as attaching a custom IsReadOnly dependency property that updates the corresponding property value on any child controls who possess that property:
public class PanelExtension : StackPanel
{
public bool IsReadOnly
{
get { return (bool)GetValue(IsReadOnlyProperty); }
set { SetValue(IsReadOnlyProperty, value); }
}
public static readonly DependencyProperty IsReadOnlyProperty =
DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(PanelExtension),
new PropertyMetadata(new PropertyChangedCallback(OnIsReadOnlyChanged)));
private static void OnIsReadOnlyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((PanelExtension)d).OnIsReadOnlyChanged(e);
}
protected virtual void OnIsReadOnlyChanged(DependencyPropertyChangedEventArgs e)
{
this.SetIsEnabledOfChildren();
}
public PanelExtension()
{
this.Loaded += new RoutedEventHandler(PanelExtension_Loaded);
}
void PanelExtension_Loaded(object sender, RoutedEventArgs e)
{
this.SetIsEnabledOfChildren();
}
private void SetIsEnabledOfChildren()
{
foreach (UIElement child in this.Children)
{
var readOnlyProperty = child.GetType().GetProperties().Where(prop => prop.Name.Equals("IsReadOnly")).FirstOrDefault();
readOnlyProperty.SetValue(child, this.IsReadOnly, null);
}
}
}
With this approach you could add as many customized properties you need, it really lends you a great deal of flexibility and enables you to account for a multitude of scenarios you might encounter when you have to set your complex permissions on various elements.

Categories