locked
How do I dynamically change an ListBoxItem's DataTemplate? RRS feed

  • Question

  • I have a WPF application with a ListBox.  I have two DataTemplates defined in Window.Resources, each with a specific x:Key value.

    When an Item is selected in the ListBox, I want to apply a different DataTemplate.  Trying to do this, I have the following code:

    1        private void ImportImagesList_SelectionChanged(object sender, SelectionChangedEventArgs e) 
    2        { 
    3            DataTemplate defaultTemplate = (DataTemplate)this.TryFindResource("ImportImageFilenamesTemplate"); 
    4            DataTemplate selectedTemplate = (DataTemplate)this.TryFindResource("ImportImageListItemTemplate"); 
    5 
    6            if (ImportImagesList.SelectedIndex >= 0) 
    7            { 
    8                ListBoxItem selected = ImportImagesList.Items[ImportImagesList.SelectedIndex] as ListBoxItem; 
    9                selected.ContentTemplate = selectedTemplate; 
    10            } 
    11        } 
    12 

    I have read about DataTemplateSelector, but I do not think this will do what I want as it appears to based on item state.  What I want is to apply a different DataTemplate to the item based on Application state (or some other factor) that is not inherent to the bound source.

    It seems this should be an easy thing to do, but I cannot get it to work.  In the code above, the DataTemplate objects are throwing null exceptions when I try to apply it to the ContentTemplate.  I cannot understand why the DataTemplate would not be found, as it is clearly defined within the Window.Resources XAML like so:

        <Window.Resources> 
            <DataTemplate x:Key="ImportImageFilenamesTemplate"
                <Border Width="Auto" Height="Auto" CornerRadius="5,5,5,5" BorderBrush="#FF1240A5" BorderThickness="1,1,2,2" Margin="2,2,4,4" MinWidth="0"
                    <Border.BitmapEffect> 
                        <DropShadowBitmapEffect/> 
                    </Border.BitmapEffect> 
                    <Grid Height="Auto" Width="Auto" Background="#FFCAD2EA"
                        <Grid.ColumnDefinitions> 
                            <ColumnDefinition Width="*"/> 
                        </Grid.ColumnDefinitions> 
                        <TextBlock Text="{Binding Path=ImageName, Mode=Default}" Height="15.96" x:Name="FileName" Width="295.308" Margin="0,2,0,0"/> 
                    </Grid> 
                </Border> 
            </DataTemplate> 
            <DataTemplate x:Key="ImportImageListItemTemplate"
                <Border Width="Auto" Height="Auto" CornerRadius="5,5,5,5" BorderBrush="#FF1240A5" BorderThickness="1,1,2,2" Margin="2,2,4,4" MinWidth="0"
                    <Border.BitmapEffect> 
                        <DropShadowBitmapEffect/> 
                    </Border.BitmapEffect> 
                    <Grid Height="Auto" Width="Auto" Background="#FFCAD2EA"
                        <Grid.ColumnDefinitions> 
                            <ColumnDefinition Width="58"/> 
                            <ColumnDefinition Width="*"/> 
                        </Grid.ColumnDefinitions> 
                        <Viewbox> 
                            <Image Width="Auto" Height="Auto" x:Name="picture" Source="{Binding Path=FullName}" MaxHeight="75" MaxWidth="75"/> 
                        </Viewbox> 
                        <StackPanel Margin="8,0,0,0" Grid.Column="1"
                            <TextBlock Text="{Binding Path=ImageName, Mode=Default}" Height="15.96" x:Name="FileName" Width="295.308" Margin="0,2,0,0"/> 
                            <TextBlock Text="{Binding Path=FullName, Mode=Default}" TextWrapping="Wrap" x:Name="FullFileName" Margin="0,2,0,0" FontStyle="Oblique"/> 
                        </StackPanel> 
                    </Grid> 
                </Border> 
            </DataTemplate> 
        </Window.Resources> 
     

    I have searched a lot but cannot find an example of this.  How can I change the DataTemplate at will?







    -- Joel Cochran - http://www.developingfor.net
    • Edited by joelcochran Tuesday, January 6, 2009 4:21 PM
    Monday, January 5, 2009 8:41 PM

Answers

  • I wrote a blog post about the final solution to this as well as an alternative, and I wanted to post it here for the archives.  The post also has a downloadable project that includes both approaches.  I'm marking this as the answer since it was a number of sources that led to the solution, but I want to give a special thanks to Dr. WPF.  His postings were a major help in me figuring this out.



    -- Joel Cochran - http://www.developingfor.net
    • Marked as answer by joelcochran Friday, January 9, 2009 7:48 PM
    Friday, January 9, 2009 7:47 PM

All replies

  • Read Dr WPF's contributions on these threads:

    switch items DataTemplate

    DataTrigger on CellTemplate

    And this thread has similar info

    Building an Editable ListBox 






    Monday, January 5, 2009 8:58 PM
  • Well, my head is surely spinning now!  I spent several hours reading about the different approaches to this problem, and I have to admit I'm feeling a little overwhelmed.  Between ControlTemplate, DataTemplate, DataTemplateSelector, ItemTemplateSelector, DataTemplate.Trigger, and more I'm just not sure which way to go.  None of it seems as straight forward as I think it should, but that's just me.

    I did get my ListBox functioning (almost) as desired.  Some of the idea came from Dr. WPF and some of it from this page at MSDN.  Below is the final solution I used.

    I finally decided on creating a DataTemplateSelector once I realized that I was not bound to only working with the individual item's properties.  I created the following DataTemplateSelector class that gets a reference to the ListBox control and then compares the current item to the ListBox's SelectedItem and returns the appropriate DataTemplate:

    public class ImportImageDataTemplateSelector : DataTemplateSelector 
        public override DataTemplate SelectTemplate(object item, DependencyObject container) 
        { 
            if (item != null && item is PictureInfo) 
            { 
                PictureInfo pic = item as PictureInfo; 
                Window window = System.Windows.Application.Current.MainWindow; 
     
                System.Windows.Controls.ListBox selected = window.FindName("ImportImagesList"as System.Windows.Controls.ListBox; 
                 
                PictureInfo selectedPic = selected.SelectedItem as PictureInfo; 
     
                if (selectedPic == null || selectedPic.FullName != pic.FullName) 
                { 
                    return window.FindResource("ImportImageFilenamesTemplate"as DataTemplate; 
                } 
                else 
                { 
                    return window.FindResource("ImportImageListItemTemplate"as DataTemplate; 
                } 
            } 
     
            return null
        } 
     

    This time, the FindResource method did not return null (I'm still not sure why it wasn't working earlier).  To implement this, I created an instance of the DataTemplateSelector in my Window.Resources section and replaced the "ItemTemplate" property of my ListBox with an "ItemTemplateSelector" property referring to that new instance:

    <Window.Resources> 
     ... 
        <prog:ImportImageDataTemplateSelector x:Key="importImageDataTemplateSelector" /> 
     ... 
        <ListBox HorizontalAlignment="Stretch" x:Name="ImportImagesList" Width="Auto" IsSynchronizedWithCurrentItem="True" SelectionChanged="ImportImagesList_SelectionChanged" ItemsSource="{Binding Path=ImportImageFileInfoList, Mode=Default}" ItemTemplateSelector="{StaticResource importImageDataTemplateSelector}"/> 
     ... 
    </Window.Resources> 
     

    I'm sure some of you more experienced guys know what's coming next: the DataTemplateSelector worked but would only fire when the ItemsSource object was initially loaded.  I found several references to this "feature" online and came across a suggestion to reset the ItemTemplateSelector whenever required: in my case, this is when the SelectionChanged event fires:

    private void ImportImagesList_SelectionChanged(object sender, SelectionChangedEventArgs e) 
        if (ImportImagesList.SelectedIndex >= 0) 
        { 
            ImportImagesList.ItemTemplateSelector = new ImportImageDataTemplateSelector(); 
        } 
     

    With these changes, the DataTemplate is now being set as desired.  I do have one bug now which may or may not be related: if I use the Mouse to select a new item in the list, it behaves as expected.  But if I use the Arrow keys to navigate the list, it ALWAYS sets the first item in the list as the SelectedItem.  Could this be because I am resetting the ItemTemplateSelector?

    I'd love some feedback: is this a good approach?  After reading, I think that a DataTemplate with nested DataTemplates and a Trigger may make more sense, but I couldn't hash out how to make it do what I wanted.  Any comments or advice would be most appreciated.



    -- Joel Cochran - http://www.developingfor.net
    Tuesday, January 6, 2009 4:17 PM
  • I wrote a blog post about the final solution to this as well as an alternative, and I wanted to post it here for the archives.  The post also has a downloadable project that includes both approaches.  I'm marking this as the answer since it was a number of sources that led to the solution, but I want to give a special thanks to Dr. WPF.  His postings were a major help in me figuring this out.



    -- Joel Cochran - http://www.developingfor.net
    • Marked as answer by joelcochran Friday, January 9, 2009 7:48 PM
    Friday, January 9, 2009 7:47 PM
  • you can do this

    <Window x:Class="WpfApplication6.Window1" 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        Title="Window1" Height="300" Width="300">  
        <Window.Resources> 
            <DataTemplate x:Key="NormalDataTemplate">  
                <TextBlock Text="{Binding Name}"></TextBlock> 
            </DataTemplate> 
            <DataTemplate x:Key="SelectedDataTemplate">  
                <StackPanel> 
                    <TextBlock Text="{Binding Name}"></TextBlock> 
                    <TextBlock Text="{Binding Address}"></TextBlock> 
                </StackPanel>              
            </DataTemplate> 
              
            <Style TargetType="{x:Type ListBoxItem}">  
                <Setter Property="ContentTemplate" 
                        Value="{StaticResource NormalDataTemplate}"></Setter> 
                <Style.Triggers> 
                    <Trigger Property="IsSelected" 
                             Value="true">  
                        <Setter Property="ContentTemplate" 
                                Value="{StaticResource SelectedDataTemplate}"></Setter> 
                    </Trigger> 
                </Style.Triggers> 
            </Style> 
        </Window.Resources> 
        <Grid> 
            <ListBox x:Name="list1"  > 
                  
            </ListBox> 
        </Grid> 
    </Window> 
     
     create a style for the listboxitem and set the ContentTemplate to defaulttemplate and when the Item is selected then it will pick up the template from the trigger
    Friday, January 9, 2009 9:41 PM
  • Hi Lee,

    My alternative solution was like that - it has two ControlTemplates, and the DataTemplate sets the simple one as the default and changes to the Complex one when IsSelected = true.  I like the triggers approach, I just need to get more comfortable with it.  Also, I think if the selection criteria were much more complex then the DataTemplateSelector path would make more sense.

    Thanks!

    -- Joel Cochran - http://www.developingfor.net
    Monday, January 12, 2009 9:40 PM
  • Hi,
    The solution is very good and I understand how to use the data template selector properly. But I have one more problem. I have some different data templates defined in Window.Resources. When an item is selected in the listbox, I want to apply a different DataTemplate without overriding the selected item.
    I mean there are two frames .The listbox is inside the upper frame. when an item is selected in the list box, the detail information regarding this item will be shown in the lower frame. But I cannot get it to work.


    I have searched a lot but cannot find the solution for this problem.  How can I change the data template like that?




    Suhlaing
    Wednesday, September 9, 2009 6:44 AM
  • Hi Suhlaing,

    It sounds like what you want is a typical List/Detail scenario.

    Define a DataTemplate for your detail area.  Let's say this area is defined by a Grid container element.  Bind the DataContext of the detail Grid to be the ListBox.SelectedItem property using OneWay binding mode.  Then you bind the elements inside the detail Grid to properties in the SelectedItem by property name.

    -- Joel Cochran - http://www.developingfor.net
    Wednesday, September 9, 2009 1:52 PM