Change input form into a display form - wpf

Some context:
Users enter data into a Window with multiple input control (standard textbox, combobox etc).
Users open same window in -readmode- displaying previously entered data.
Sure, input form is easy and for readmode I can use the IsEnabled dependancyproperty to disable the input controls.
Is it possible to replace all input controls with labels using Style with Triggers?

This will turn all TextBoxes into TextBlocks when IsReadOnly is true. As you guessed, there's a trigger on the Style that changes the control template.
<Window x:Class="SO_Xaml_ReadOnlyInputForm.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.Resources>
<Style TargetType="TextBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsReadOnly}"
Value="True">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<TextBlock Text="{TemplateBinding Text}"
Width="{TemplateBinding Width}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<CheckBox Grid.Row="0"
IsChecked="{Binding IsReadOnly}"
Content="Is Read-only?" />
<StackPanel Grid.Row="1"
Orientation="Horizontal">
<TextBlock>Item1</TextBlock>
<TextBox Text="{Binding Item1Text}"
Width="100" />
</StackPanel>
</Grid>
</Window>
VIEWMODEL class
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.Prism.ViewModel;
using Microsoft.Practices.Prism.Commands;
namespace SO_Xaml_ReadOnlyInputForm
{
public class ViewModel : NotificationObject
{
private string _itemText;
private bool _isReadOnly;
public string Item1Text
{
get { return _itemText; }
set
{
_itemText = value;
RaisePropertyChanged(() => Item1Text);
}
}
public bool IsReadOnly
{
get { return _isReadOnly; }
set
{
_isReadOnly = value;
RaisePropertyChanged(() => IsReadOnly);
}
}
public ViewModel()
{
Item1Text = "This is the text";
}
}
}

Related

UserControl multiple datatemplate + templateselector

I need to show data inside a usercontrol in different ways depending on a flag.
To achieve this i tried the following, but using this control in the main view shows nothing.
<UserControl DataContext="**self**">
<UserControl.Resources>
<DataTemplate x:Key="mouseInputTemplate">
<TextBlock HorizontalAlignment="Center"><Run Text="{Binding Text}" /></TextBlock>
</DataTemplate>
<DataTemplate x:Key="touchInputTemplate">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image Source="{Binding ImageUri}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Width="{Binding ImageWidth}" Height="{Binding ImageHeight}" />
<TextBlock HorizontalAlignment="Center"><Run Text="{Binding Text}" /></TextBlock>
</StackPanel>
</DataTemplate>
<local:InputModeDataTemplateSelector x:Key="inputModeTemplateSelector"
MouseInputModeTemplate="{StaticResource mouseInputTemplate}"
TouchInputModeTemplate="{StaticResource touchInputTemplate}" />
</UserControl.Resources>
<ContentControl HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch" ContentTemplateSelector="{StaticResource inputModeTemplateSelector}" />
</UserControl>
What am i doing wrong?
Is there a better way to achieve that?
Thank to EdPlunkett and more research i found out it is better to
use a ContentPresenter here and instead of binding on DataContext=this bind like this (as alsways suggested when writing a UserControl)
DataContext="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type yourType}}}"
Code:
<UserControl.Resources>
<DataTemplate x:Key="touchInputTemplate">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image Source="{Binding ImageUri}" Width="64" Height="64" />
<TextBlock HorizontalAlignment="Center" Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="mouseInputTemplate">
<TextBlock HorizontalAlignment="Center" Text="{Binding Text}" />
</DataTemplate>
<local:InputModeDataTemplateSelector x:Key="inputModeTemplateSelector"
MouseInputModeTemplate="{StaticResource mouseInputTemplate}"
TouchInputModeTemplate="{StaticResource touchInputTemplate}" />
</UserControl.Resources>
<Grid DataContext="{Binding RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type yourType}}}">
<ContentPresenter Content="{Binding}" ContentTemplateSelector="{StaticResource inputModeTemplateSelector}">
</Grid>
Your ContentPresenter idea is the correct way to do it with a DataTemplateSelector, and I should have thought of it myself.
But here's yet another way to do it, which unlike my first answer, actually solves all the problems you're having:
XAML (in practice the Style would probably be defined in a separate ResourceDictionary):
<Window
x:Class="TestApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestApplication"
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<Style TargetType="local:TestControl">
<Setter Property="Background" Value="Gainsboro" />
<Style.Triggers>
<!-- The 0 value for the InputMode enum is Mouse, so this will be the default. -->
<Trigger Property="InputMode" Value="Mouse">
<Setter Property="Background" Value="Wheat" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TestControl}">
<Grid Background="{TemplateBinding Background}">
<TextBlock HorizontalAlignment="Center"><Run Text="{TemplateBinding Text}" /></TextBlock>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="InputMode" Value="Touch">
<Setter Property="Background" Value="LightSkyBlue" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:TestControl}">
<Grid Background="{TemplateBinding Background}">
<StackPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Image
Source="{TemplateBinding ImageUri}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Width="{TemplateBinding ImageWidth}"
Height="{TemplateBinding ImageHeight}"
/>
<TextBlock HorizontalAlignment="Center"><Run Text="{TemplateBinding Text}" /></TextBlock>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<local:TestControl
ImageHeight="100"
ImageWidth="100"
Text="This is the test control"
ImageUri="http://www.optimizeagency.com/wp-content/uploads/2015/09/GoogleLogo.jpg"
/>
</Grid>
</Window>
C#:
using System;
using System.Windows;
using System.Windows.Controls;
namespace TestApplication
{
class TestControl : Control
{
public TestControl()
{
// If input mode may change at runtime, you'll need an event that fires when that
// happens and updates this property.
// UIUtilities.GetInputMode() is just a stub in this example.
InputMode = UIUtilities.GetInputMode();
}
#region InputMode Property
public InputMode InputMode
{
get { return (InputMode)GetValue(InputModeProperty); }
set { SetValue(InputModeProperty, value); }
}
public static readonly DependencyProperty InputModeProperty =
DependencyProperty.Register("InputMode", typeof(InputMode), typeof(TestControl),
new PropertyMetadata(InputMode.Mouse));
#endregion InputMode Property
#region Text Property
public String Text
{
get { return (String)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(String), typeof(TestControl),
new PropertyMetadata(null));
#endregion Text Property
#region ImageUri Property
// The TemplateBinding in the template can't coerce a string to an
// ImageSource, so we have to make that happen elsewhere.
public ImageSource ImageUri
{
get { return (ImageSource)GetValue(ImageUriProperty); }
set { SetValue(ImageUriProperty, value); }
}
public static readonly DependencyProperty ImageUriProperty =
DependencyProperty.Register("ImageUri", typeof(ImageSource), typeof(TestControl),
new PropertyMetadata(null));
#endregion ImageUri Property
#region ImageHeight Property
public float ImageHeight
{
get { return (float)GetValue(ImageHeightProperty); }
set { SetValue(ImageHeightProperty, value); }
}
public static readonly DependencyProperty ImageHeightProperty =
DependencyProperty.Register("ImageHeight", typeof(float), typeof(TestControl),
new PropertyMetadata(float.NaN));
#endregion ImageHeight Property
#region ImageWidth Property
public float ImageWidth
{
get { return (float)GetValue(ImageWidthProperty); }
set { SetValue(ImageWidthProperty, value); }
}
public static readonly DependencyProperty ImageWidthProperty =
DependencyProperty.Register("ImageWidth", typeof(float), typeof(TestControl),
new PropertyMetadata(float.NaN));
#endregion ImageWidth Property
}
#region This stuff belongs in a different file
public static class UIUtilities
{
public static InputMode GetInputMode()
{
// Here you'd do whatever you're already doing to detect the input mode
return InputMode.Touch;
}
}
public enum InputMode
{
Mouse,
Touch
}
#endregion This stuff belongs in a different file
}

How to make my control focusable?

I have a control
public class FocusTestControl : Control {
public FocusTestControl() {
DefaultStyleKey = typeof(FocusTestControl);
}
}
Here is it's default style
<Style
TargetType="local:FocusTestControl">
<Setter
Property="Focusable"
Value="True" />
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Border
Background="AliceBlue" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I put this control on a window:
<Window
x:Class="MakeWpfControlFocusable.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MakeWpfControlFocusable"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition
Height="35" />
</Grid.RowDefinitions>
<local:FocusTestControl />
<StackPanel
Grid.Row="1"
Orientation="Horizontal">
<TextBlock
Text="Focused element: " />
<TextBlock
Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}, Path=KeyboardFocusedElement}" />
<TextBox
Text="Text" />
</StackPanel>
</Grid>
But clicking on control doesn't make it focused (I mean KebordFocus)
Actually, my task is handle the KeyDown and KeyUp events. But it is impossible when element has no keybord focus.
Keybord focus can be set by pessing Tab keyboard key. But is is not set when click on the control. I've subscribed to the MouseDown event and set focus manually in the handler.
public class FocusTestControl : Control, IInputElement {
public FocusTestControl() {
DefaultStyleKey = typeof(FocusTestControl);
MouseDown += FocusTestControl_MouseDown;
}
void FocusTestControl_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e) {
Keyboard.Focus(this);
}
}

XAML to add header to radio button

So with a lot of looking around I am hoping to make a GroupBox that acts like a Radio button. The header section would act as the bullet. I took some code from this question
Styling a GroupBox
that is how I want it to look. But I want to have it as a Radio button. So I put in this code (mind you I've only been doing WPF for a week or 2 now)
<Style TargetType="{x:Type RadioButton}" >
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type RadioButton}">
<BulletDecorator>
<BulletDecorator.Bullet>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border x:Name="SelectedBorder"
Grid.Row="0"
Margin="4"
BorderBrush="Black"
BorderThickness="1"
Background="#25A0DA">
<Label x:Name="SelectedLabel" Foreground="Wheat">
<ContentPresenter Margin="4" />
</Label>
</Border>
<Border>
</Border>
</Grid>
</BulletDecorator.Bullet>
</BulletDecorator>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="true">
<Setter TargetName="SelectedBorder" Property="Background" Value="PaleGreen"/>
<Setter TargetName="SelectedLabel"
Property="Foreground"
Value="Black" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I have a feeling that I can add a label to the second row of my grid, but then I don't know how to access it. I have that template in a test project in the Window.Resources section (I plan on moving it to a resource dictionary in my main project)
the xaml for my window is this
<Grid>
<GroupBox Name="grpDoor" Margin ="8" Grid.Row="0" Grid.Column="0">
<GroupBox.Header>
WPF RadioButton Template
</GroupBox.Header>
<StackPanel Margin ="8">
<RadioButton FontSize="15" Content="Dhaka" Margin="4" IsChecked="False"/>
<RadioButton FontSize="15" Content="Munshiganj" Margin="4" IsChecked="True" />
<RadioButton FontSize="15" Content="Gazipur" Margin="4" IsChecked="False" />
</StackPanel>
</GroupBox>
</Grid>
I then hoping for something like this (not sure how I'd do it yet though)
<Grid>
<GroupBox Name="grpDoor" Margin ="8" Grid.Row="0" Grid.Column="0">
<GroupBox.Header>
WPF RadioButton Template
</GroupBox.Header>
<StackPanel Margin ="8">
<RadioButton FontSize="15"
Content="Dhaka"
Margin="4"
IsChecked="False">
<RadioButton.Description>
This is a description that would show under my Header
</RadioButton.Description>
</RadioButton>
<RadioButton FontSize="15"
Content="Munshiganj"
Margin="4"
IsChecked="True">
<RadioButton.Description>
This is a description that would show under my Header
</RadioButton.Description>
</RadioButton>
<RadioButton FontSize="15"
Content="Gazipur"
Margin="4"
IsChecked="False">
<RadioButton.Description>
This is a description that would show under my Header
</RadioButton.Description>
</RadioButton>
</StackPanel>
</GroupBox>
</Grid>
Based on your clarification, here is a very simple example with a RadioButton that looks like a GroupBox.
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:SimpleViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type local:SimpleOption}">
<RadioButton GroupName="choice" IsChecked="{Binding Path=IsSelected, Mode=TwoWay}">
<RadioButton.Template>
<ControlTemplate TargetType="{x:Type RadioButton}">
<GroupBox x:Name="OptionBox" Header="{Binding Path=DisplayName, Mode=OneWay}">
<TextBlock Text="{Binding Path=Description, Mode=OneWay}"/>
</GroupBox>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected, Mode=OneWay}" Value="True">
<Setter TargetName="OptionBox" Property="Background" Value="Blue"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</RadioButton.Template>
</RadioButton>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemsSource="{Binding Path=Options, Mode=OneWay}"/>
</Grid>
</Window>
public class SimpleViewModel
{
public SimpleViewModel()
{
Options = new ObservableCollection<SimpleOption>();
var _with1 = Options;
_with1.Add(new SimpleOption {
DisplayName = "Dhaka",
Description = "This is a description for Dhaka."
});
_with1.Add(new SimpleOption {
DisplayName = "Munshiganj",
Description = "This is a description for Munshiganj.",
IsSelected = true
});
_with1.Add(new SimpleOption {
DisplayName = "Gazipur",
Description = "This is a description for Gazipur."
});
}
public ObservableCollection<SimpleOption> Options { get; set; }
}
public class SimpleOption : INotifyPropertyChanged
{
public string DisplayName {
get { return _displayName; }
set {
_displayName = value;
NotifyPropertyChanged("DisplayName");
}
}
private string _displayName;
public string Description {
get { return _description; }
set {
_description = value;
NotifyPropertyChanged("Description");
}
}
private string _description;
public bool IsSelected {
get { return _isSelected; }
set {
_isSelected = value;
NotifyPropertyChanged("IsSelected");
}
}
private bool _isSelected;
private void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged;
public delegate void PropertyChangedEventHandler(object sender, PropertyChangedEventArgs e);
}
I'd do it with a custom attached property. That way, you can bind to it from a ViewModel, or apply it directly in XAML.
First, create a new class in your Style assembly:
public static class RadioButtonExtender
{
public static readonly DependencyProperty DescriptionProperty = DependencyProperty.RegisterAttached(
"Description",
typeof(string),
typeof(RadioButtonExtender),
new FrameworkPropertyMetadata(null));
[AttachedPropertyBrowsableForType(typeof(RadioButton))]
public static string GetDescription(RadioButton obj)
{
return (string)obj.GetValue(DescriptionProperty);
}
public static void SetDescription(RadioButton obj, string value)
{
obj.SetValue(DescriptionProperty, value);
}
}
And your style's Bullet would change so that the label is:
<Label x:Name="SelectedLabel"
Foreground="Wheat"
Content="{Binding (prop:RadioButtonExtender.Description), RelativeSource={RelativeSource TemplatedParent}} />
You could then use it in your final XAML:
<RadioButton FontSize="15"
Content="Dhaka"
Margin="4"
IsChecked="False">
<prop:RadioButtonExtender.Description>
This is a description that would show under my Header
</prop:RadioButtonExtender.Description>
</RadioButton>
As an added bonus, since you're creating the Style in a separate assembly, you can create a custom XAML namespace to make using your property easier.

Slow wpf ItemsControl refresh on ListCollectionView

The problem I have is my listcollectionview is taking 3-4 seconds to update. I think I have the virtualization configured correctly; however, please point out if it is incorrect. I have my itemssource bound to a ICollectionView.
If i set the loop to 3000+ in BindingViewModel UI takes a long time to launch even though it takes less than a second to build the ViewModels.
I don't know how to attach files so I'll post all the sample code that reproduces this problem:
BigList.Xaml:
`
<Style x:Key="textBlockBaseStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="Blue"/>
</Style>
<Style x:Key="headerButtonStyle" TargetType="Button">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="Blue"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="DarkBlue"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="borderBaseStyle" TargetType="Border">
<Setter Property="BorderBrush" Value="Blue"/>
</Style>
<!--these two styles let us cascadingly style on the page without having to specify styles
apparently styles don't cascade into itemscontrols... in the items control we must reference via key -->
<Style TargetType="TextBlock" BasedOn="{StaticResource textBlockBaseStyle}"/>
<Style TargetType="Border" BasedOn="{StaticResource borderBaseStyle}"/>
<!-- hover styles -->
<Style x:Key="onmouseover" TargetType="{x:Type Border}" BasedOn="{StaticResource borderBaseStyle}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="DarkBlue"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<StackPanel Width="390">
<!-- Header-->
<Border BorderThickness="1,1,1,1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="55"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="70"/>
<ColumnDefinition Width="70"/>
</Grid.ColumnDefinitions>
<Button Command="{Binding SortFirstNameCommand}" Style="{StaticResource headerButtonStyle}" Grid.Column="1" >
<TextBlock TextAlignment="Left" Text="First"/>
</Button>
<Button Style="{StaticResource headerButtonStyle}" Grid.Column="2" >
<TextBlock TextAlignment="Left" Text="Last"/>
</Button>
<Button Style="{StaticResource headerButtonStyle}" Grid.Column="3" >
<TextBlock TextAlignment="Left" Text="Activity"/>
</Button>
<Button Style="{StaticResource headerButtonStyle}" Grid.Column="4">
<TextBlock TextAlignment="Left" Text="Last Activity"/>
</Button>
</Grid>
</Border>
<Border BorderThickness="1,0,1,1">
<ItemsControl VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling" ItemsSource="{Binding UsersAvailForEmail}" MaxHeight="400">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer x:Name="ScrollViewer" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto" IsDeferredScrollingEnabled="True" Padding="{TemplateBinding Padding}">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource onmouseover}" BorderThickness="0,0,1,1">
<CheckBox Margin="20,5" IsChecked="{Binding IsSelected}">
<CheckBox.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Width="20"/>
<TextBlock Width="70" Style="{StaticResource textBlockBaseStyle}" Grid.Column="1" Text="{Binding FirstName}"/>
<TextBlock Width="70" Style="{StaticResource textBlockBaseStyle}" Grid.Column="2" Text="{Binding LastName}"/>
<TextBlock Width="70" Style="{StaticResource textBlockBaseStyle}" Grid.Column="3" Text="{Binding Action}"/>
<TextBlock Width="60" Style="{StaticResource textBlockBaseStyle}" Grid.Column="4" Text="{Binding ActionId}"/>
</StackPanel>
</CheckBox.Content>
</CheckBox>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</StackPanel>
</Grid>
`
BindingViewModel.cs:
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows.Data;
namespace LargeItemsCollection
{
public class BindingViewModel : INotifyPropertyChanged
{
ObservableCollection<EmailPersonViewModel> _usersAvail = new ObservableCollection<EmailPersonViewModel>();
ListCollectionView _lvc = null;
public BindingViewModel()
{
for (int i = 0; i < 4000; i++)
{
_usersAvail.Add( new EmailPersonViewModel { FirstName = "f" +i.ToString() , LastName= "l" + i.ToString(), Action= "a" + i.ToString(), ActionId="ai" + i.ToString(), IsSelected=true });
}
_lvc = new ListCollectionView(_usersAvail);
}
public ICollectionView UsersAvailForEmail
{
get { return _lvc; }
}
/// <summary>
/// Raised when a property on this object has a new value.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises this object's PropertyChanged event.
/// </summary>
/// <param name="propertyName">The property that has a new value.</param>
public virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
var e = new PropertyChangedEventArgs(propertyName);
handler(this, e);
}
}
}
EmailPersonViewModel.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace LargeItemsCollection
{
public class EmailPersonViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Action { get; set; }
public string ActionId { get; set; }
public bool IsSelected { get; set; }
public EmailPersonViewModel()
{
}
}
}
MainWindow.xaml:
<Window x:Class="LargeItemsCollection.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LargeItemsCollection"
Title="MainWindow" Height="350" Width="525">
<Grid>
<local:BigList/>
</Grid>
</Window>
MainWindow.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace LargeItemsCollection
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new BindingViewModel();
}
}
}
All righty per this post:
Virtualizing a WPF ItemsControl
I didn't have the Scrollviewer.CanScroll='true' in WPF virtualizationstackpanel doesn't seem to work without it.
Thanks all who posted solutions to help me along the way. You guys didn't have the solution so I didn't mark it as correct; however, I gave you guys upvotes because you both helped along the way. Here's the final XAML so you can see what the itemscontrol needs to look like:
<ItemsControl ScrollViewer.CanContentScroll="True" VirtualizingStackPanel.VirtualizationMode="Recycling" ItemsSource="{Binding UsersAvailForEmail}" MaxHeight="400">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer x:Name="ScrollViewer" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto" IsDeferredScrollingEnabled="True" Padding="{TemplateBinding Padding}">
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Style="{StaticResource onmouseover}" BorderThickness="0,0,1,1">
<CheckBox Margin="20,5" IsChecked="{Binding IsSelected}">
<CheckBox.Content>
<StackPanel Orientation="Horizontal">
<TextBlock Width="20"/>
<TextBlock Width="70" Style="{StaticResource textBlockBaseStyle}" Grid.Column="1" Text="{Binding FirstName}"/>
<TextBlock Width="70" Style="{StaticResource textBlockBaseStyle}" Grid.Column="2" Text="{Binding LastName}"/>
<TextBlock Width="70" Style="{StaticResource textBlockBaseStyle}" Grid.Column="3" Text="{Binding Action}"/>
<TextBlock Width="60" Style="{StaticResource textBlockBaseStyle}" Grid.Column="4" Text="{Binding ActionId}"/>
</StackPanel>
</CheckBox.Content>
</CheckBox>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The virtualizing stack panel virtualizes creation of objects in the visual tree, not the objects in the ItemsSource. If it takes 4 seconds to create the collection of view model objects that the control is bound to, it's going to take 4 seconds to display the control irrespective of whether or not it uses virtualization.
Since you're sorting the collection, you have to instantiate all of the items in the collection before the control can display any of them. However long it takes to construct all 4,000 objects is a fixed cost here.
An easy way to test this is to create a command that builds the collection of objects and a second command that displays the items control bound to the collection. Then you can watch in the UI and see how long it takes to do each of those things separately.
If this is in fact the problem, you can focus on speeding up object creation (e.g. by using lazy evaluation on properties that are time-consuming to access, assuming they're not used by the sort), or possibly populate the collection in the background.
I guess it's due to the sorting approach that you've taken. Under the hood it uses reflection and this is quite a bottleneck. Consider using CustomSort property of the ListCollectionView class. More links in this answer.
Hope this helps.

How do I trigger a WPF expander IsExpanded property from another expander's IsExpanded property

I have two expanders, side by side. I want only one to be expanded at a time. So if one is expanded, and the user expands the other, I want the first one to collapse. The user can have both collapsed, and both collapsed is the starting state.
As can be seen in the code, I have included the "Header" property as a test, and it works as expected, but the IsExpanded property is not working.
<Expander x:Name="emailExpander">
<Expander.Style>
<Style TargetType="Expander">
<Setter Property="IsExpanded" Value="False"/>
<Setter Property="Header" Value="Email"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsExpanded,ElementName=customerExpander}" Value="True">
<Setter Property="IsExpanded" Value="False"/>
<Setter Property="Header" Value="other expanded"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Expander.Style>
</Expander>
This can be handled by binding to a view object with a little logic added.
In your WPF bind the IsExpanded property to the EmailExpanded and CustomerExpanded properties of the view.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Expander Grid.Column="0" Header="Email" IsExpanded="{Binding EmailExpanded}">
<TextBlock Text="Email Data"/>
</Expander>
<Expander Grid.Column="1" Header="Customer" IsExpanded="{Binding CustomerExpanded}">
<TextBlock Text="Customer Data"/>
</Expander>
</Grid>
Assign the view in your main Window.
public MainWindow()
{
InitializeComponent();
DataContext = new View();
}
Then make your view class something like the following.
class View : INotifyPropertyChanged
{
private bool _CustomerExpanded;
public bool CustomerExpanded
{
get
{
return _CustomerExpanded;
}
set
{
if (_CustomerExpanded != value)
{
// Add logic to close Email Expander
if (value)
{
EmailExpanded = false;
}
_CustomerExpanded = value;
OnPropertyChanged("CustomerExpanded");
}
}
}
private bool _EmailExpanded;
public bool EmailExpanded
{
get
{
return _EmailExpanded;
}
set
{
if (_EmailExpanded != value)
{
// Add logic to close Customer Expander
if (value)
{
CustomerExpanded = false;
}
_EmailExpanded = value;
OnPropertyChanged("EmailExpanded");
}
}
}
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
Notice the addition to the setters. Collapsing an expander will have no effect on the other expander, but expanding one will cause the other to collapse. No stack overflow :)
I found the answer in this post:
WPF Expanders Triggers
Use BoolInverterConverter in the answer above and here is the code snippets for your case
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:BoolInverterConverter x:Key="bic"/>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Expander x:Name="emailExpander" IsExpanded="{Binding ElementName=customerExpander, Converter={StaticResource bic}, Path=IsExpanded}">
<Expander.Style>
<Style TargetType="Expander">
<Setter Property="Header" Value="Email"/>
</Style>
</Expander.Style>
<StackPanel Margin="10,4,0,0">
<CheckBox Margin="4" Content="Email 1" />
<CheckBox Margin="4" Content="Email 2" />
<CheckBox Margin="4" Content="Email 3" />
</StackPanel>
</Expander>
<Expander x:Name="customerExpander" Grid.Column="1">
<Expander.Style>
<Style TargetType="Expander">
<Setter Property="Header" Value="Customer"/>
</Style>
</Expander.Style>
<StackPanel Margin="10,4,0,0">
<CheckBox Margin="4" Content="Customer 1" />
<CheckBox Margin="4" Content="Customer 2" />
<CheckBox Margin="4" Content="Customer 3" />
</StackPanel>
</Expander>
</Grid>
What you're better off doing is use an accordion control released in the WPF Toolkit V2. Very handy and no "Stack Overflow" exceptions. =)

Resources