Search code examples
wpfdatatemplatedynamicresourcestaticresource

WPF staticresource references to Logical Resources in DataTemplates not resolving at runtime


Have I missed something in the step up from .net 3.5 to .net 4 because I'm seeing seemingly buggy behaviour that seems contrary to the goal of the system.

I am trying to drum up a simple MVVM library for work using a few examples. I am consuming it within a Twitter client application for some additional learning and have hit a big stumbling block.

The scenario is this. My root ViewModel (TwitterClientViewModel) object is given an instance of a DialogViewModel object for display. The DialogViewModel is added to a collection and a bool HasDialogs is set to true. PropertyChanged events are invoked for the collection and the flag if necessary. This part works fabulously.

The view for TwitterClientViewModel is called TwitterClientTemplate and makes Visible an overlay for DialogViewTemplate (DialogViewModel's view) hosting. The hosting ContentControl's template references DialogViewTemplate with a DynamicResource extension. This displays great in the designer and at runtime.

This is where things get strange. The 'body' of DialogViewTemplate hosts dialog content with a further content control bound to DialogViewModel.Content (type object). The hope was that with use of a TemplateSelector (of which I wrote a nice declarative one, but have commented out for testing purposes) I could display both text and interactive elements. For example, requesting details from the user when authenticating a Twitter account. In this case, a PIN number.

At this point I have a two nested contentcontrols for the dialog implementation. For testing purposes, the contentcontrol in the body of DialogViewTemplate uses a staticresource extension to retrieve EnterPINDialogTemplate (view for EnterPINDialogViewModel). Both EnterPINDialogTemplate and DialogViewTemplate are in the same file (the former is defined first of course) although originally they were separate.

At runtime the staticresource extension throws a XamlParseException with the message; 'Provide value on 'System.Windows.Markup.StaticResourceHolder' threw an exception.'

and an inner exception message;

'Cannot find resource named 'EnterPINDialogTemplate'. Resource names are case sensitive'

Using a dynamicresource returns null and displays the Fullname of the EnterPINDialogViewModel type in the contentcontrol - as expected when the resource is not resolved. Breaking into my custom TemplateSelector as calling FrameWorkElement.FindResource() throws a similar exception (TryFindResource returns null).

My first thought was that the logical tree is split when the datatemplates are constructed and I remembered an issue in that area from an earlier project. I tried using the MergeDictionaries property of ResourceDictionary to make the resource dictionaries avaliable from within the DataTemplate but the designer did not like that one bit, and the error is described here: http://connect.microsoft.com/VisualStudio/feedback/details/498844/wpf-designer-throws-invalidcastexception

Scratch that idea. I have tried merging the dictionaries at Application, Window and TwitterClientTemplate levels but have had no luck.

Below are the xaml files.

DialogTemplates.xaml

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:EpicTweet.ViewModel" 
xmlns:ET="clr-namespace:EpicTweet"
xmlns:T="clr-namespace:EpicTweet.Tools"
xmlns:MV="clr-namespace:MVVM;assembly=MVVM"
xmlns:Loc="clr-namespace:EpicTweet.Localization"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
<DataTemplate DataType="VM:EnterPINDialogViewModel" x:Key="EnterPINDialogTemplate">
    <Grid d:DesignWidth="453.89" d:DesignHeight="78.92" Loc:ResXManagerProperty.ResourceManager="{x:Static ET:Language.ResourceManager}">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Label Content="{Loc:ResxExtension ResourceName=String_PIN, FallbackValue='&lt;PIN&gt;'}"/>
        <TextBox Grid.Column="1"/>
        <TextBlock Grid.Row="1" Grid.RowSpan="2"></TextBlock>
    </Grid>
</DataTemplate>
<DataTemplate x:Key="DialogViewTemplate" DataType="MV:DialogViewModel">
    <Border BorderBrush="Black" BorderThickness="1">
        <Grid d:DesignWidth="277.419" d:DesignHeight="74.96" Background="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}" Height="Auto" Width="Auto">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <Border d:LayoutOverrides="Width, Height" BorderThickness="0,0,0,1" BorderBrush="Black">
                <Label Content="{Binding DisplayName, FallbackValue=Header}" VerticalAlignment="Center" HorizontalAlignment="Left"/>    
            </Border>
            <ContentControl Content="{Binding Content, FallbackValue=Body}" ContentTemplate="{StaticResource EnterPINDialogTemplate}" HorizontalAlignment="Stretch" d:LayoutOverrides="Height" Grid.Row="1" Margin="5">
                <!--<ContentControl.ContentTemplateSelector>
                    <T:TypeTemplateSelector>
                        <T:TemplateTypeRelationship Type="{x:Type VM:EnterPINDialogViewModel}" ResourceKey="EnterPINDialogTemplate"/>
                    </T:TypeTemplateSelector>
                </ContentControl.ContentTemplateSelector>-->
            </ContentControl>
                <ItemsControl Grid.Row="2" Margin="10" 
                ItemsSource="{Binding Commands, Mode=OneTime, FallbackValue={x:Static VM:TwitterClientViewModel.DEFAULT_DIALOG_COMMANDS}}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate>
                        <Button 
                        Content="{Binding DisplayName, FallbackValue=CommandName, Mode=OneWay}"
                        Command="{Binding}"/>
                    </DataTemplate>
                </ItemsControl.ItemTemplate>
                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <WrapPanel Orientation="Horizontal"/>
                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>
            </ItemsControl>
        </Grid>
    </Border>
</DataTemplate>

TwitterClientDataTemplate.xaml

<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:VM="clr-namespace:EpicTweet.ViewModel" 
xmlns:ET="clr-namespace:EpicTweet"
xmlns:MV="clr-namespace:MVVM;assembly=MVVM"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d">
<ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="DialogTemplates.xaml"/>
</ResourceDictionary.MergedDictionaries>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
<DataTemplate x:Key="TwitterClientTemplate" DataType="MV:TwitterClientViewModel">
    <ScrollViewer d:DesignWidth="285.083" d:DesignHeight="119.96">
        <Grid>
            <StackPanel d:LayoutOverrides="Width, Height">
                <StackPanel Orientation="Horizontal">
                    <Button Command="{Binding AddAccountCommand.Command}" Content="{Binding AddAccountCommand.DisplayName, FallbackValue=&lt;Add Account&gt;}"/>
                </StackPanel>
                <ContentControl/>
            </StackPanel>
            <Border BorderThickness="1" Background="#80000000" Visibility="{Binding HasDialogs, Converter={StaticResource BooleanToVisibilityConverter}, FallbackValue=Collapsed, Mode=OneWay}">
                <Grid VerticalAlignment="Stretch" MinWidth="50" MaxWidth="200">
                    <ContentControl Content="{Binding Dialogs[0], Mode=OneWay}" ContentTemplate="{DynamicResource DialogViewTemplate}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
                </Grid>
            </Border>
        </Grid>
    </ScrollViewer>
</DataTemplate>

Help me stackoverflow, you're my only hope!

EDIT: Did some further work on this issue. If both templates are in the same file dynamicresource and staticresource extensions both resolve the resource without issue. If they are in separate files, the resource will not resolve regardless of how I merge the dictionaries; each extension returns null.

Obviously the solution is to throw both resources into the same dictionary but as far as I'm concerned this is a hack and is not intended behaviour of the logical resource lookup system. I am not a happy bunny right now. This seems pretty undocumented...


Solution

  • If ever there was a pratt, it is me. After 4 hours on a Friday night trying to solve this one I have cracked it, no thanks to what I can only call flaky error reporting.

    Here is the buzz.

    <DataTemplate x:Key="TwitterClientTemplate" DataType="MV:TwitterClientViewModel">
    

    should be

    <DataTemplate x:Key="TwitterClientTemplate" DataType="{x:Type MV:TwitterClientViewModel}">
    

    And bam, it works.

    My big gripe remains however. Why does the incorrect syntax work in the designer but not at runtime? My guess is because runtime optimization just doesn't bother to populate a dictionary with poorly authored xaml but it would be nice to get a warning that it is wrong.