TemplateBinding in a nested template in Silverlight 4 - silverlight

I've implemented a control, CommandTextBox, which I want to be a text box with a button right next to it (so it almost appears within the text box).
The button should be an image which I can bind to an icon. It's fairly straightfoward stuff...
public class CommandTextBox : TextBox
{
/// <summary>
/// The image property.
/// </summary>
public static readonly DependencyProperty ImageProperty = DependencyProperty.Register(
"Image", typeof(ImageSource), typeof(CommandTextBox), null);
/// <summary>
/// Initializes a new instance of the <see cref = "CommandTextBox" /> class.
/// </summary>
public CommandTextBox()
{
this.DefaultStyleKey = typeof(CommandTextBox);
}
/// <summary>
/// Gets or sets the image.
/// </summary>
/// <value>
/// The image.
/// </value>
public ImageSource Image
{
get
{
return (ImageSource)this.GetValue(ImageProperty);
}
set
{
this.SetValue(ImageProperty, value);
}
}
}
I have a template as follows...
<Style TargetType="Controls:CommandTextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Controls:CommandTextBox">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{TemplateBinding Text}"/>
<Button Grid.Column="1"
Content="Search" >
<Button.Template>
<ControlTemplate>
<Image Source="{TemplateBinding Image}" />
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
But I get an error due to the template binding in the image. I sortof understand why, it's because the Template has changed now so the binding context isn't the same but I don't know how to overcome it.
Do I need to create a seperate ImageButton control so I can just do a normal template binding or is there another way?
Thanks
Ben

I have managed to get this to work by changing the style as follows:
<Style TargetType="appControls:CommandTextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="appControls:CommandTextBox">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{TemplateBinding Text}"/>
<Button Grid.Column="1" >
<Button.Content>
<Image DataContext="{TemplateBinding Image}" Source="{Binding}" />
</Button.Content>
</Button>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I'm not using a seperate template for the Button. My XAML with the control is:
<controls:CommandTextBox Text="Text" Image="/MyApp.Silverlight;component/Assets/Images/Amber_Triangle.png"></controls:CommandTextBox>
This appears to achieve the result you were after. The control renders on my test page as expected.

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
}

WPF Change a property in a customcontrol after a button click

I posted a question in this link. maybe I'm not well expressed.
It's very simple, I want to change a property in a usercontrol or CustomControl after a click on a Boutton outside...
The code of the customcontrol is as follows :
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<Border x:Name="container" Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
<ControlTemplate.Triggers>
<Trigger Property="Hidden" Value="true">
<Setter Property="BorderBrush" Value="Blue"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
public class CustomControl1 : Control
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public bool Hidden
{
get { return (bool)GetValue(HiddenProperty); }
set { SetValue(HiddenProperty, value); }
}
// Using a DependencyProperty as the backing store for Hidder. This enables animation, styling, binding, etc...
public static readonly DependencyProperty HiddenProperty =
DependencyProperty.Register("Hidden", typeof(bool), typeof(CustomControl1), new PropertyMetadata(false));
}
And a simple window for test
<Window x:Class="WpfTestCustomControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfCustomBorder;assembly=WpfCustomBorder"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="70"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<local:CustomControl1 x:Name="cc" BorderBrush="Red" BorderThickness="3" Margin="10" Grid.RowSpan="2"/>
<Button Grid.Column="1" Content="Ok" Margin="5" Click="Button_Click"/>
</Grid>
namespace WpfTestCustomControl
{
/// <summary>
/// Logique d'interaction pour MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
cc.Hidden = true;
}
}
}
The property "Hidden" is a dependency property inside the custom control.
When i click on the button in mainwindow i want to change the hidden property to true. this must fire the trigger inside the custom control to change borderbrush to "blue" color. While nothing happen.
Is there something missing or is not the right way to do it ?
Thanks in advance..
Don't hard-set BorderBrush="Red" in your Control's declaration, it's prioritary over any trigger's setter.
You might want to check msdn's Dependency Property Value Precedence

Icon badge overlay for notifications in silverlight/xaml

I've got a ribbon bar in my silverlight app and on one of the icons I would like for there to be a badge icon showing the number of items in the view that the icon activates.
Picture the Mail icon in OS X showing the number of unread messages or the notifications counter on an IOS app icon.
I don't know much about xaml styles, but it seems to me I could duplicate the default style for the ribbon bar button, then add to it with some sort of red circle, and a white text that took in its value from a new property on the ribbon bar button somehow so I would be able to bind to it.
Does anyone have an example of something like this I can start from?
Thanks Shawn for the answer. This is what I ended up doing:
In the xaml:
<telerikRibbonBar:RadRibbonRadioButton
Text="Expired Active Call Factors"
Size="Large"
LargeImage="/CallFactorDatabase.UI;component/Images/Ribbon/Large/ExpiredActiveView.png"
Command="{Binding ActivateViewCommand}"
CommandParameter="ExpiredActiveView">
<Grid>
<Grid.Resources>
<converters:BooleanToVisibilityConverter x:Key="visibleWhenTrueConverter" VisibilityWhenTrue="Visible" VisibilityWhenFalse="Collapsed" />
</Grid.Resources>
<Grid Width="27" Height="27" Visibility="{Binding ExpiredActiveCallFactors, Converter={StaticResource visibleWhenTrueConverter}}" Margin="50,-40,0,0">
<Ellipse Fill="Black" Width="27" Height="27"/>
<Ellipse Width="25" Height="25" VerticalAlignment="Center" HorizontalAlignment="Center">
<Ellipse.Fill>
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="Coral" Offset="0.0" />
<GradientStop Color="Red" Offset="1.0" />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<Viewbox Width="25" Height="25" VerticalAlignment="Center" HorizontalAlignment="Center" >
<TextBlock Text="{Binding ExpiredActiveCallFactorsCount}" Foreground="White"/>
</Viewbox>
</Grid>
</Grid>
</telerikRibbonBar:RadRibbonRadioButton>
How it looks:
No luck getting it in front of the ribbon button but oh well.
This can be accomplished with a few bindings and an optional value converter. This samples assumes you are binding to a model that has an Items property and that that property is of type ObservableCollection so that the Count property of the collection will fire property changed when items are added/removed.
<Grid>
<Grid.Resources>
<local:CountToVisbilityConverter x:Key="CountToVis"/>
</Grid.Resources>
....
<Grid Width="25" Height="25" Visibility="{Binding Items.Count, Converter=CountToVis}">
<Ellipse Fill="Red" Width="25" Height="25"/>
<ViewBox Width="25" Height="25">
<TextBlock Text="{Binding Itmes.Count}" Foreground="White"/>
</Viewbox>
</Grid>
</Grid>
And the value converter:
public class CountToVisibilityConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if(value == null) return Visibility.Collapsed;
int count = System.Convert.ToInt32(value);
return count == 0 ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
The reason I saw optional" converter is because you can also use the Interaction DataTriggers like such
<Grid x:Name="UnreadNotification" Width="25" Height="25">
<Ellipse Fill="Red" Width="25" Height="25"/>
<ViewBox Width="25" Height="25">
<TextBlock Text="{Binding Itmes.Count}" Foreground="White"/>
</Viewbox>
</Grid>
<i:Interaction.Triggers>
<ei:DataTrigger Binding="{Binding Items.Count, Comparison="Equal"
Value="0">
<ei:ChangePropertyAction PropertyName="IsEnabled"
Value="True"
TargetName="UnreadNotification" />
</ei:DataTrigger>
</i:Interaction.Triggers>
Here's my solution for this. By default, the badge will show in the upper right corner. You can change this by setting the "BadgeMarginOffset" property. I've attached a couple of images to show how it appears. One, which shows the badge wrapping a Telerik RadRibbonButton. You can also change the background and foreground colors of the badge (BadgeBackground, BadgeForeground). The defaults are shown below.
The UserControl's XAML
<UserControl x:Class="Foundation.Common.Controls.Wpf.Badge"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:converters="clr-namespace:Foundation.Common.Controls.Wpf.Converters"
Background="Transparent" x:Name="UserControl"
mc:Ignorable="d" >
<UserControl.Resources>
<converters:GreaterThanZeroBooleanConverter x:Key="GreaterThanZeroBooleanConverter" />
<converters:GreaterThanZeroVisibilityConverter x:Key="GreaterThanZeroVisibilityConverter"/>
</UserControl.Resources>
<UserControl.Template>
<ControlTemplate>
<Grid HorizontalAlignment="Stretch" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border CornerRadius="10"
UseLayoutRounding="True"
x:Name="BadgeBorder"
Grid.ZIndex="99"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Visibility="{Binding ElementName=UserControl, Path=Count, Mode=TwoWay, Converter={StaticResource GreaterThanZeroVisibilityConverter}}"
Grid.Row="0"
Margin="{Binding ElementName=UserControl, Path=BadgeMarginOffset}"
Height="22"
Padding="6.7,2,7,3"
Background="{Binding ElementName=UserControl, Path=BadgeBackground}">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=UserControl, Path=Count, Mode=TwoWay, Converter={StaticResource GreaterThanZeroBooleanConverter}}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation From="0.0"
To="1.0"
Duration="0:0:0.7"
Storyboard.TargetProperty="Opacity"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
<DataTrigger Binding="{Binding ElementName=UserControl, Path=ShowDropShadow}" Value="True">
<Setter Property="Effect">
<Setter.Value>
<DropShadowEffect BlurRadius="6" ShadowDepth="4" Color="#949494"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding ElementName=UserControl, Path=Count}"
Foreground="{Binding ElementName=UserControl, Path=BadgeForeground}"
FontWeight="Bold"
FontSize="12">
</TextBlock>
</Border>
<ContentPresenter Grid.Row="0"
Grid.RowSpan="2"
DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=DataContext}"
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}" />
</Grid>
</ControlTemplate>
</UserControl.Template>
The UserControl's code behind
public partial class Badge : UserControl
{
#region Dependency Properties
public static readonly DependencyProperty CountProperty =
DependencyProperty.Register("Count", typeof(int), typeof(Badge));
public static readonly DependencyProperty ShowDropShadowProperty =
DependencyProperty.Register("ShowDropShadow", typeof(bool), typeof(Badge), new PropertyMetadata(true));
public static readonly DependencyProperty BadgeMarginOffsetProperty =
DependencyProperty.Register("BadgeMarginOffset", typeof(Thickness), typeof(Badge));
public static readonly DependencyProperty BadgeBackgroundProperty =
DependencyProperty.Register("BadgeBackground", typeof(Brush), typeof(Badge), new PropertyMetadata(Brushes.Red));
public static readonly DependencyProperty BadgeForegroundProperty =
DependencyProperty.Register("BadgeForeground", typeof(Brush), typeof(Badge), new PropertyMetadata(Brushes.White));
#endregion Dependency Properties
#region Constructor
/// <summary>
/// Initializes a new instance of the <see cref="Badge"/> class.
/// </summary>
public Badge()
{
this.InitializeComponent();
}
#endregion Constructor
#region Properties
/// <summary>
/// Gets or sets a value indicating whether [show drop shadow].
/// </summary>
/// <value>
/// <c>true</c> if [show drop shadow]; otherwise, <c>false</c>.
/// </value>
public bool ShowDropShadow
{
get => (bool)this.GetValue(ShowDropShadowProperty);
set => this.SetValue(ShowDropShadowProperty, value);
}
/// <summary>
/// Gets or sets the badge margin offset.
/// </summary>
/// <value>
/// The badge margin offset.
/// </value>
public Thickness BadgeMarginOffset
{
get => (Thickness)this.GetValue(BadgeMarginOffsetProperty);
set => this.SetValue(BadgeMarginOffsetProperty, value);
}
/// <summary>
/// Gets or sets the badge background.
/// </summary>
/// <value>
/// The badge background.
/// </value>
public Brush BadgeBackground
{
get => (Brush)this.GetValue(BadgeBackgroundProperty);
set => this.SetValue(BadgeBackgroundProperty, value);
}
/// <summary>
/// Gets or sets the badge foreground.
/// </summary>
/// <value>
/// The badge foreground.
/// </value>
public Brush BadgeForeground
{
get => (Brush)this.GetValue(BadgeForegroundProperty);
set => this.SetValue(BadgeBackgroundProperty, value);
}
/// <summary>
/// Gets or sets the count.
/// </summary>
/// <value>
/// The count.
/// </value>
public int Count
{
get => (int)this.GetValue(CountProperty);
set => this.SetValue(CountProperty, value);
}
#endregion Properties
}
Sample code for the top two images
<wpf:Badge Count="108" Margin="20" HorizontalAlignment="Left" BadgeMarginOffset="0,-5,-5,0">
<Button Height="100" Width="100">
<Button.Content>
<Image Source="Resources/about.png" />
</Button.Content>
</Button>
</wpf:Badge>

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