Performance problem with multiple ItemsControl inside a ScrollViewer - silverlight

I need a single scrollable surface that contains two bound lists. At first, I used a ScrollViewer with two ListBox inside, each having their scrolling disabled, so I could still have item selection. Seeing the poor loading time performance, I changed my ListBoxes to ItemsControl, but the performance is still terrible. In total, my two lists have only 110 items.
<ScrollViewer Grid.Row="1">
<StackPanel>
<Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
<TextBlock Text="{Binding Resources.CurrentLocationItem, Source={StaticResource LocalizedResources}}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
</Button>
<TextBlock Text="{Binding Resources.TopTenCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
<ItemsControl ItemsSource="{Binding TopTenCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
<TextBlock Text="{Binding Resources.TopHundredCitiesHeader, Source={StaticResource LocalizedResources}}" Style="{StaticResource PhoneTextSubtleStyle}" Margin="12,12,12,8" />
<ItemsControl ItemsSource="{Binding TopHundredCities}" ItemTemplate="{StaticResource CityDataTemplate}" HorizontalContentAlignment="Stretch" />
</StackPanel>
</ScrollViewer>
What can I do to improve performance? I've tried setting the ItemsSource after the page loading, but it still ugly (empty lists for a few seconds), doesn't make more sense.
Thank you.

This answer has turned into a monster but slog through it and I think you'll find an answer.
We need in some way to use the VirtualizingStackPanel as ListBox. We need to collect all the items to display (the button, the two textblocks and two sets of city data) into a single enumerable of some type. The the real trick and would be to determine one of three templates to use to render the items.
Bottom line is we need to create a new type of ItemsControl. Now we can gain a little advantage by simply accepting we want to create a very specific ItemsControl that supports only this task. First here is a "starter for 10" (a UK media reference).
A really dumb example of creating a specific items control:-
public class SwitchingItemsControl : ItemsControl
{
public DataTemplate AlternativeItemTemplate { get; set; }
protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
ContentPresenter cp = (ContentPresenter)element;
if (AlternativeItemTemplate != null && (((int)item) & 1) == 1)
cp.ContentTemplate = AlternativeItemTemplate;
else
cp.ContentTemplate = ItemTemplate;
cp.Content = item;
}
}
This control assumes its items are a set of integers. It has an AlternativeItemTemplate which if supplied it toggles between on an odd/even basis (note that is a facet of the item).
Now lets put that use with a VirtualizingStackPanel:-
<UserControl x:Class="CustomVirtualizingPanelInSL.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SilverlightApplication1"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<local:SwitchingItemsControl x:Name="itemsControl" >
<local:SwitchingItemsControl.Template>
<ControlTemplate TargetType="local:SwitchingItemsControl">
<ScrollViewer VerticalScrollBarVisibility="Visible">
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</local:SwitchingItemsControl.Template>
<local:SwitchingItemsControl.ItemTemplate>
<DataTemplate>
<Border CornerRadius="2" BorderBrush="Blue" BorderThickness="1" Margin="2">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
</Border>
</DataTemplate>
</local:SwitchingItemsControl.ItemTemplate>
<local:SwitchingItemsControl.AlternativeItemTemplate>
<DataTemplate>
<Border CornerRadius="2" BorderBrush="Red" BorderThickness="1" Margin="2">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Center" Text="{Binding}" />
</Border>
</DataTemplate>
</local:SwitchingItemsControl.AlternativeItemTemplate>
<local:SwitchingItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</local:SwitchingItemsControl.ItemsPanel>
</local:SwitchingItemsControl>
</Grid>
</UserControl>
Note the ItemsPanel is using the VirtualizingStackPanel and that gets presented in a ScrollViewer.
Now we can give it lot of content:-
namespace SilverlightApplication1
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
itemsControl.ItemsSource = Enumerable.Range(0, 10000);
}
}
}
If you switch to a standard StackPanel this takes ages to load, whereas it appears instant with virtualizing.
Armed with this info you should be able to create a special ItemsControl which has the properties:-
ButtonTemplate (DataTemplate)
HeaderTemplate (DataTemplate)
TopTenHeaderText (String)
TopHundredHeaderText (String)
TopTenSource (IEnumerable<City>)
TipHunderedSource (IEnumerable<City>)
Now you can create a single enumerable with some Linq extension methods:-
itemsControl.ItemsSource = Enumerable.Repeat((object)null, 1)
.Concat(Enumerable.Repeat((object)TopTenHeadeText))
.Concat(TopTenSource.Cast<object>())
.Concat(Enumerable.Repeat((object)TopHundredText))
.Concat(TopHundredSource.Cast<object>())
Now you just need to override PrepareContainerForItemOverride and choose between ButtonTemplate (for the first null item), the HeaderTemplate for item of type string or the ItemTemplate for an item of type City.

Thank you #AnthonyWJones, your answer was (almost) exactly what I was looking for. I've decided to provide my own answer here so that other readers know how I've adapted his answer to my needs.
First, as suggested, I'm deriving from ItemsControl, and providing a second "Template" property, called HeaderTemplate:
#region HeaderTemplate PROPERTY
public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register(
"HeaderTemplate",
typeof( DataTemplate ),
typeof( ItemsControlWithHeaders ),
new PropertyMetadata( null, new PropertyChangedCallback( OnHeaderTemplateChanged ) ) );
public DataTemplate HeaderTemplate
{
get { return ( DataTemplate )this.GetValue( HeaderTemplateProperty ); }
set { this.SetValue( HeaderTemplateProperty, value ); }
}
private static void OnHeaderTemplateChanged( DependencyObject obj, DependencyPropertyChangedEventArgs args )
{
ItemsControlWithHeaders control = obj as ItemsControlWithHeaders;
control.InvalidateArrange();
}
#endregion
Second, I'm overriding PrepareContainerForItemOverride to provide my own template selection logic. What I'm doing is simply redirecting any "string" item to the HeaderTemplate, and other items to the usual ItemTemplate:
protected override void PrepareContainerForItemOverride( DependencyObject element, object item )
{
base.PrepareContainerForItemOverride( element, item );
ContentPresenter presenter = element as ContentPresenter;
if( presenter != null )
{
if( item is string )
{
presenter.ContentTemplate = this.HeaderTemplate;
}
else
{
presenter.ContentTemplate = this.ItemTemplate;
}
}
}
This control can now be used like this:
<local:ItemsControlWithHeaders Grid.Row="1" ItemsSource="{Binding GroupedCities}" ScrollViewer.VerticalScrollBarVisibility="Auto">
<local:ItemsControlWithHeaders.Template>
<ControlTemplate TargetType="local:ItemsControlWithHeaders">
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</ControlTemplate>
</local:ItemsControlWithHeaders.Template>
<local:ItemsControlWithHeaders.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</local:ItemsControlWithHeaders.ItemsPanel>
<local:ItemsControlWithHeaders.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Style="{StaticResource PhoneTextSubtleStyle}" Foreground="{StaticResource PhoneAccentBrush}" Margin="12,12,12,8" />
</DataTemplate>
</local:ItemsControlWithHeaders.HeaderTemplate>
<local:ItemsControlWithHeaders.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource EmptyNonSelectButtonStyle}" BorderThickness="0" HorizontalContentAlignment="Left" Click="AnyCityButton_Click">
<TextBlock Text="{Binding Name, Mode=OneWay}" FontFamily="{StaticResource PhoneFontFamilyNormal}" FontSize="{StaticResource PhoneFontSizeLarge}" />
</Button>
</DataTemplate>
</local:ItemsControlWithHeaders.ItemTemplate>
</local:ItemsControlWithHeaders>
To build the data source you must pass to this special hybrid control, LINQ is fine, but I've chosen a much more explicit solution, implemented in my view-model:
public IEnumerable<object> GroupedCities
{
get
{
yield return new CurrentLocationCityViewModel();
yield return Localized.TopTenCitiesHeader; // string resource
foreach( CityViewModel city in this.TopTenCities )
{
yield return city;
}
yield return Localized.TopHundredCitiesHeader; // string resource
foreach( CityViewModel city in this.TopHundredCities )
{
yield return city;
}
}
}
I now have a generic ItemsControlWithHeaders I can reuse in more than just this scenario. Performance is great. The only problem remaining for a purist like me is that the base ItemsControl complains in DEBUG, since an "object" type does not have a "Name" property. It generates a System.Windows.Data Error: BindingExpression path error: 'Name' property not found message in the debug output, which can be ignored.

Could you use one list/itemscontrol, but different datatemplates to get the same effect?
Or you could use a pivot control instead, putting the top 10 sitties in one pivot, top 100 in another pivot..

Related

MVVM WPF Combobox: creating a template for comboboxitem

I have a WPF combobox which is populated from code-behind.
Code-behind (xaml.cs):
namespace WpfApplication1
{
private ObservableCollection<TransportType> transportTypes = new ObservableCollection<TransportType>();
transportTypes.Add(new TransportType() {Icon = Properties.Resources.Air, ValueMember = "A100", DisplayMember = "By Air" });
transportTypes.Add(new TransportType() {Icon = Properties.Resources.Maritime, ValueMember = "M200", DisplayMember = "Maritime" });
this.ComboBoxTransportTypes.ItemsSource = transportTypes;
}
TransportType class:
namespace WpfApplication1
{
public class TransportType
{
public Image Icon
{
get;
set;
}
public string DisplayMember
{
get;
set;
}
public string ValueMember
{
get;
set;
}
}
}
View:
<ComboBox x:Name="ComboBoxTransportTypes"
Grid.Column="1"
ItemsSource="{Binding}"
DisplayMemberPath="DisplayMember"
SelectedValuePath="ValueMember"
SelectionChanged="ComboBoxTransportTypes_SelectionChanged">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
</ComboBox>
Now I am trying to apply a ComboBox ItemTemplate and bound to the "transportTypes" collection. I would like each combobox item to be as below:
<ComboBoxItem>
<StackPanel Orientation="Horizontal">
<Image Source="{Binding bind-icon-here}" />
<TextBlock Foreground="AliceBlue"
VerticalAlignment="Center"
Text="{Binding bind-DisplayMember-here}"/>
</StackPanel>
</ComboBoxItem>
So how can I create the above combobox item template bound to my collection in order to each item to be presented with an icon followed by a string?
I have tried below but it does not work. I also do not know how to bind each item in the collection to the image and textblock within stackpanel, I have done as below but only string is displayed and not icon.
<ComboBox x:Name="ComboBoxTransportTypes"
Grid.Column="1"
ItemsSource="{Binding}"
DisplayMemberPath="DisplayMember" <-- removed from here as I cannot define DisplayMemberPath and item template at the same time.
SelectedValuePath="ValueMember"
SelectionChanged="ComboBoxTransportTypes_SelectionChanged">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate DataType="l:TransportType">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Icon}" />
<TextBlock Foreground="AliceBlue"
VerticalAlignment="Center"
Text="{Binding DisplayMember}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
Also, in MVVM is it better to populate combobox from code-behind as I have done here or from view model constructor?
The problem is that you're setting the ItemSource in both the XAML and in the code behind. If you remove ItemSource="{Binding}" from the XAML then it should work.
If you are using MVVM, the collection should be populated in the view model, not in the code behind. There should be very little code in your code behind - only things related to the view should go there (such as displaying a child window).
The problem is that Image.Source takes an ImageSource not an Image. Change ...
<Image Source="{Binding Icon}" />
to...
<Frame Content="{Binding Icon}"/>
and things will start working.
you must use bottom syntax for custom template ComboBox
<TextBlock Text="{Binding Path=DisplayMember}"/>
or
<Image Source="{Binding Path=Icon}" />
in wpf list tags like combobox,listbox,... for custom tamplate like DataTemplate you must use Path

WPF Textbox TwoWay binding in datatemplate not updating the source even on LostFocus

I have an ObservableCollection<string> Tags as part of a custom object. I bind it to a DataTemplate in order to show all tags to the user with the following code:
<StackPanel DockPanel.Dock="Top" Margin="15,0,15,0" Orientation="Horizontal">
<Label Content="Tags:" FontSize="14" Foreground="{StaticResource HM2LightTextBrush}"/>
<Grid>
<ItemsControl Name="PanelPreviewNoteTags" ItemsSource="{Binding ElementName=lbNotesQuickView, Path=SelectedItem.Tags}" Margin="3,0" Visibility="Collapsed">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1" BorderBrush="#676B6E" Margin="3,0">
<Label Content="{Binding .,Mode=OneWay}" Foreground="{StaticResource HM2LightTextBrush}"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl Name="PanelEditNoteTags" ItemsSource="{Binding ElementName=lbNotesQuickView, Path=SelectedItem.Tags}" Margin="3,0" Visibility="Collapsed">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderThickness="1" BorderBrush="#676B6E" Margin="3,0">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding ., Mode=TwoWay}"/>
<Button Style="{StaticResource RibbonButton}" Click="ButtonRemoveTagClick" Tag="{Binding}">
<Image Height="16" Width="16" Source="/Poker Assistant;component/Resources/fileclose.png" />
</Button>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</StackPanel>
Adding and removing items from the ObservableCollection works as expected.
In code I switch between edit and view mode by setting the Visibility of the corresponding PanelEditNoteTags and PanelPreviewNoteTags. This all good and working. But when I enter the edit mode and start typing new values for the tags in the TextBox the source doesn't get updated. I certainly know that the LostFocus event is raised when I press my Save button. I tried all UpdateSourceTrigger values, still the same.
Is it a problem related to two controls binding at the same time to the same value - the Label from PanelPreviewNoteTags and the TextBox from PanelEditNoteTags?
What am I missing here?
#Clemens Thank you for the quick and accurate response :) Following is the working solution for future reference.
The solution is not to use ObservableCollection<string> Tags because as pointed by Clemens the {Binding ., Mode=TwoWay} does not work back to the source.
So I created a custom Tag class:
public class Tag : INotifyPropertyChanged
{
private string _content;
public string Content { get { return _content; } set { _content = value; OnMyPropertyChanged(() => Content); } }
public Tag(string content)
{ Content = content; }
public Tag()
: this("new tag")
{ }
public event PropertyChangedEventHandler PropertyChanged;
// Raise the event that a property has changed in order to update the visual elements bound to it
internal void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
//CONVERTS the passed parameter to its name in string
internal void OnMyPropertyChanged<T>(Expression<Func<T>> memberExpression)
{
MemberExpression expressionBody = (MemberExpression)memberExpression.Body;
OnPropertyChanged(expressionBody.Member.Name);
}
public override string ToString()
{
return Content;
}
}
And use it as ObservableCollection<Tag> Tags. Then bind to it like so
<TextBox Text="{Binding Content, Mode=TwoWay}" Tag="{Binding}"/>
I actually populate from and save to postgres database in a string array column, so I need to convert to and from string[]. These are my conversions:
string[] array = note.Tags.Select(item => item.Content).ToArray();
note.Tags = new ObservableCollection<Tag>((array.Select(item => new Tag() { Content = item }).ToList()));

ListBox with WrapPanel - Remove item / arrow key navigation

I have a strange problem regarding my listbox / wrappanel arrow navigation.
My Listbox :
<ListBox x:Name="List"
IsSynchronizedWithCurrentItem="True"
SelectedIndex="{Binding MainIndex, Mode=OneWayToSource}"
SelectedItem="{Binding CurrentFeed, Mode=TwoWay}"
ItemsSource="{Binding CurrentFeedList}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel IsItemsHost="True" MaxWidth="{Binding ActualWidth, ElementName=Panel}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Width="168">
<Border BorderBrush="White" BorderThickness="0" Margin="0,7,0,0">
<Image Source="{Binding Image , Converter={StaticResource ByteArraytoImageSource}}" Width="150" Height="213" ></Image>
</Border>
<Border BorderBrush="White" BorderThickness="0" Height="65" HorizontalAlignment="Center" >
<TextBlock VerticalAlignment="Center" TextWrapping="Wrap" FontFamily="{StaticResource DefaultFontFamily}" FontSize="15" Text="{Binding Title}" Foreground="White" Cursor="Hand" HorizontalAlignment="Center" TextAlignment="Center"/>
</Border>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
The render :
(source: free.fr)
The render is great No problem ! ;)
As you can see it's a list of wallpaper retrieved from RSS.
I have bind a shortcut to make one item readed and then it's removed from the ObservableCollection :
<UserControl.InputBindings>
<KeyBinding Key="D" Modifiers="Control" Command="{Binding ReadCmd}" />
</UserControl.InputBindings>
In My View Model :
ObservableCollection<Feed> CurrentFeedList { get; set; }
private Feed _currentFeed;
public Feed CurrentFeed
{
get { return _currentFeed; }
set
{
_currentFeed = value;
OnPropertyChanged("CurrentFeed");
}
}
public ICommand ReadCmd { get; set; }
public int MainIndex { get; set; }
My ReadCmd is a RelayCommand that call "ReadAction" method.
At the beginning I simply remove the item from the ObservableCollection, but I want to type Ctrl+D twice to read 2 item.
So I decided to get the index of the listbox and select back the same index after removing one.
private void ReadAction()
{
int previousIndex = MainIndex;
if (previousIndex == CurrentFeedList.Count - 1)
previousIndex -= 1;
CurrentFeedList.Remove(CurrentFeed);
CurrentFeed = CurrentFeedList[previousIndex];
}
It's work as I wanted, but one problem remain and I can't solve it.
When I push Ctrl+D the SelectedItem is remove and the next item become Selected. But then when I use the arrow key navigation to navigate over the list, it jump each time to the first item of the list.
I hope it's enough clear.
Thanks you.
This issue is due to the fact that when you are removing the items using the KeyBinding, ListBox loses the focus and hence pressing the arrow keys after that places focus on first item of the ListBox.
So in order to make your arrow keys work from the selected item you will also have to set the focus on Selected Item of the Listbox.
There are couple of ways of doing this, e.g using attached behavior to set the focus on the ListBoxItem. Simplest would be to capture the SelectionChanged event of your ListBox and in the handler set the focus on the currently selected item like below:
private void MyListBox_OnSelectionChanged(object sender, RoutedEventArgs e)
{
var item = (ListBoxItem)MyListBox.ItemContainerGenerator.ContainerFromItem(MyListBox.SelectedItem);
if(item !=null)
item.Focus();
}

How to make custom control with ListBox?

I want to make horizontal ListBox with customized item template, so I make a basic template of it.
However, I couldn't find an example of binding 'things' to that WPF XAML, especially with ListBox filled with customized items.
I simply want to dynamically add/remove items in the ListBox with Image, Label, ComboBox with previously filled with number 1 to 10.
the ADD/REMOVE button will be placed outside WPF control, it means that the buttons will be on the Main Window Form.
Also, there are TextBox and picture selector in the Main Window Form so that I can change the text and image.
Below is code behind XAML :
Public Class listSequence
Public Sub New()
InitializeComponent()
listbox.Items.Add("hi")
listbox.Items.Add("there")
End Sub
End Class
Below is XAML :
<ListBox Name="listbox" VerticalContentAlignment="Stretch" ScrollViewer.VerticalScrollBarVisibility="Disabled" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.CanContentScroll="True" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Stretch" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Padding="10" Margin="5" BorderThickness="1" BorderBrush="Aqua" CornerRadius="0" Width="120" VerticalAlignment="Stretch">
<StackPanel>
<Image />
<Label Content="{Binding}" />
<TextBlock Text="hi" />
<ComboBox x:Name="cboRepeat" ItemsSource="{Binding}" DisplayMemberPath="RepeatCounter" IsSynchronizedWithCurrentItem="True"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Let's say we have a basic class named Item:
public class Item : INotifyPropertyChanged
{
public string Text { get; set; } // Implement INotifyPropertyChanged
public string ImagePath { get; set; } // properly on these properties
}
And a collection of these in a view model:
public ObservableCollection<Item> Items { get; set; }
Now to display these items in the UI, we use a ListBox and set the ItemsSource property:
<ListBox ItemsSource="{Binding Items}">
</ListBox>
When it comes to defining the ListBox.ItemTemplate, you need to understand that this DataTemplate will be applied to each item and that it has access to all of the properties defined in the Item class:
<ListBox ItemsSource="{Binding Items}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Image ImageSource="{Binding ImagePath}" />
<TextBlock Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Therefore, you can access the properties in the collection class as shown above. You can find out the full story by looking at the ItemsControl.ItemTemplate Property page on MSDN.

Binding to a viewmodel property in a DataTemplate

I'm fairly new to XAML but enjoying learning it. The thing I'm really struggling with is binding a property to an element in a DataTemplate.
I have created a simple WPF example to, (hopefully,) explain my problem.
I this example I am trying to bind the Visibility property of a CheckBox in a DataTemplate to a Property in my viewmodel. (Using this scenario purely for learning/demo.)
I have a simple DataModel named Item, but is of little relevance in this example.
class Item : INotifyPropertyChanged
{
// Fields...
private bool _IsRequired;
private string _ItemName;
And a fairly simple View Model named ItemViewModel.
class ItemViewModel : INotifyPropertyChanged
{
private ObservableCollection<Item> _Items;
private bool _IsCheckBoxChecked;
private bool _IsCheckBoxVisible;
public ObservableCollection<Item> Items
{
get { return _Items; }
set { _Items = value; }
}
public bool IsCheckBoxChecked
{
get { return _IsCheckBoxChecked; }
set
{
if (_IsCheckBoxChecked == value)
return;
_IsCheckBoxChecked = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("IsCheckBoxChecked"));
PropertyChanged(this, new PropertyChangedEventArgs("IsCheckBoxVisible"));
}
}
}
public bool IsCheckBoxVisible
{
get { return !_IsCheckBoxChecked; }
set
{
if (_IsCheckBoxVisible == value)
return;
_IsCheckBoxVisible = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsCheckBoxVisible"));
}
(Constructors and INotifyPropertyChanged implementation omitted for brevity.)
Controls laid out in MainPage.xaml as follows.
<Window.Resources>
<local:VisibilityConverter x:Key="VisibilityConverter"/>
</Window.Resources>
<Window.DataContext>
<local:ItemViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<CheckBox x:Name="checkBox" Content="Hide CheckBoxes" FontSize="14" IsChecked="{Binding IsCheckBoxChecked, Mode=TwoWay}" />
<ListView ItemsSource="{Binding Items}" HorizontalContentAlignment="Stretch" >
<ListView.ItemTemplate >
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding ItemName}"/>
<CheckBox Grid.Column="1" Visibility="{Binding IsCheckBoxVisible, Converter={StaticResource VisibilityConverter}}" >
<CheckBox.DataContext>
<local:ItemViewModel/>
</CheckBox.DataContext>
</CheckBox>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel Orientation="Horizontal" Margin="4,4,0,0">
<TextBlock Text="IsCheckBoxVisible:"/>
<TextBlock Text="{Binding IsCheckBoxVisible}" Margin="4,0,0,0" FontWeight="Bold" />
</StackPanel >
<Button Content="Button" Visibility="{Binding IsCheckBoxVisible, Converter={StaticResource VisibilityConverter}}" Margin="4,4,4,4"/>
</StackPanel>
</Grid>
The 'Hide CheckBoxes' checkbox is bound to IsCheckBoxChecked and is used to update IsCheckBoxVisible. I've also added a couple of extra controls below the DataTemplate to prove, (to myself,) the everything works.)
I have also implemented Jeff Wilcox's value converter. (Thank you.) http://www.jeff.wilcox.name/2008/07/visibility-type-converter/
When I run the app, checking and unchecking the 'Hide Checkbox', controls outside the DataTemplate function as expected but, alas, the Checkbox inside the data template remains unchanged.
I have had success with:
IsVisible="{Binding IsChecked, Converter={StaticResource VisibilityConverter}, ElementName=checkBox}"
But I'm not just trying mimic another control but make decisions based on a value.
I would REALLY appreciate any help or advice you can offer.
Thank you.
When you are in a DataTemplate, your DataContext is the data templated object, in this case an Item. Thus, the DataContext of the CheckBox in the DataTemplate is an Item, not your ItemViewModel. You can see this by your <TextBlock Text="{Binding ItemName}"/>, which binds to a property on the Item class. The Binding to IsCheckBoxVisible is trying to find a property called IsCheckBoxVisible on Item.
There are a couple of ways around this, but by far the easiest is to do this:
On your Window (in the xaml), give it and x:Name. Eg:
<Window [...blah blah...]
x:Name="MyWindow">
Change your binding to look like this:
<CheckBox Grid.Column="1"
Visibility="{Binding DataContext.IsCheckBoxVisible, ElementName=MyWindow, Converter={StaticResource VisibilityConverter}}">
We're using the Window as the source for the Binding, then looking at its DataContext property (which should be your ItemViewModel, and then pulling off the IsCheckBoxVisible property.
Another option, if you want something fancier, is to use a proxy object to reference your DataContext. See this article on DataContextProxy.

Resources