WPF DataTriggers on ContentControls are firing after their bound Dependency Property's PropertyChangedCallback

312 views Asked by At

I'm coding a WPF input dialog window, that will show a different Control based on a dependency property named InputType. The language I'm using is Visual COBOL .NET, but the issue is not related to the language but to WPF itself, and the language is easily understandable by VB and C# programmers. This is the XAML code for my dialog window

<Window x:Name="wndDialog"
    x:Class="ClassLibraryNew.AGInputBox"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:lib="clr-namespace:ClassLibraryNew"
    xmlns:ctrl="clr-namespace:ClassLibraryNew.Controls"
    Width="400"
    MinHeight="200"
    WindowStyle="None"
    WindowStartupLocation="CenterOwner"
    ResizeMode="NoResize"
    Background="#FFEEEEEE"
    SizeToContent="Height"
    MouseDown="OnMouseDown"
    Loaded="OnLoaded">
<Window.CommandBindings>
    <CommandBinding Command="{x:Static lib:DialogCommands.OkCommand}" CanExecute="OnCommandCanExecute" Executed="OnCommandExecuted"/>
    <CommandBinding Command="{x:Static lib:DialogCommands.CancelCommand}" CanExecute="OnCommandCanExecute" Executed="OnCommandExecuted"/>
</Window.CommandBindings>
<Border Style="{StaticResource AGTWindowBorder}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <GroupBox Grid.Row="0" Header="{Binding Caption}">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="*"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0"
                           Text="{Binding Text}"
                           FontSize="{Binding Converter={StaticResource FontSizeConverter}, ConverterParameter='16'}"
                           VerticalAlignment="Center"
                           TextWrapping="Wrap"/>

                <ContentControl Grid.Row="1" x:Name="contentControl" >
                    <ContentControl.Style>
                        <Style TargetType="{x:Type ContentControl}">
                            <Setter Property="ContentTemplate">
                                <Setter.Value>
                                    <DataTemplate>
                                        <TextBlock Foreground="Red" Text="Errore: input type non valido. Contattare l'assistenza tecnica."/>
                                    </DataTemplate>
                                </Setter.Value>
                            </Setter>
                            <Style.Triggers>
                                <DataTrigger Binding="{Binding InputType}" Value="Text">
                                    <Setter Property="ContentTemplate">
                                        <Setter.Value>
                                            <DataTemplate>
                                                <ctrl:TextField Text="{Binding Value, ElementName=wndDialog}"
                                                                ctrl:WatermarkService.Watermark="{Binding WatermarkText, ElementName=wndDialog}"
                                                                ctrl:WatermarkService.HideWhenFocused="False"
                                                                MaxLength="{Binding MaxLength, ElementName=wndDialog}"/>
                                            </DataTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </DataTrigger>
                                <DataTrigger Binding="{Binding InputType}" Value="Integer">
                                    <Setter Property="ContentTemplate">
                                        <Setter.Value>
                                            <DataTemplate>
                                                <ctrl:IntegerField Value="{Binding Value, ElementName=wndDialog}"
                                                                   ZeroFill="{Binding ZeroFill, ElementName=wndDialog}"
                                                                   MaxLength="{Binding MaxLength, ElementName=wndDialog}"/>
                                            </DataTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </DataTrigger>
                                <DataTrigger Binding="{Binding InputType}" Value="Decimal">
                                    <Setter Property="ContentTemplate">
                                        <Setter.Value>
                                            <DataTemplate>
                                                <ctrl:DecimalField Value="{Binding Value, ElementName=wndDialog}"
                                                                   DecimalDigits="{Binding DecimalDigits, ElementName=wndDialog}"/>
                                            </DataTemplate>
                                        </Setter.Value>
                                    </Setter>
                                </DataTrigger>
                            </Style.Triggers>
                        </Style>
                    </ContentControl.Style>
                </ContentControl>
            </Grid>
        </GroupBox>
        <StackPanel Grid.Row="1" 
                    Orientation="Horizontal" 
                    HorizontalAlignment="Right"
                    Margin="8">
            <StackPanel.Resources>
                <Style TargetType="{x:Type Button}" BasedOn="{StaticResource EuroButton}">
                    <Setter Property="Width" Value="80"/>
                    <Setter Property="Margin" Value="8,0,0,0"/>
                </Style>
            </StackPanel.Resources>
            <Button IsDefault="True"
                    Command="{x:Static lib:DialogCommands.OkCommand}">
                <AccessText Text="_Ok"/>
            </Button>
            <Button IsCancel="True"
                    Command="{x:Static lib:DialogCommands.CancelCommand}">
                <AccessText Text="_Annulla"/>
            </Button>
        </StackPanel>
    </Grid>
</Border>

What I want to achieve is showing the user a 'TextField' (custom TextBox) when the InputType is Text, 'IntegerField' when the InputType is Integer, etc.. InputType's type is an enum name DialogInputType which contains three values (Text, Integer, Decimal). This works fine, however I need a way to to attach an event handler to the Field inside the ContentControl when its content has been correctly set and is not null. I expected the DataTriggers to re-evaluate when the InputType changed, instead this fails: (Visual COBOL .NET)

   01  InputTypeProperty type DependencyProperty public static initialize only
       value type DependencyProperty::Register(
           "InputType",
           type of DialogInputType,
           type of AGInputBox,
           new FrameworkPropertyMetadata(
               type DialogInputType::Text,
               new PropertyChangedCallback(method OnInputTypeChanged)
           )
       ).
   *> property definition omitted...

   method-id OnInputTypeChanged private static.
   procedure division using by value sender as type DependencyObject, by value e as type DependencyPropertyChangedEventArgs.
       if sender not instance of type AGInputBox
           goback
       end-if
       declare wnd as type AGInputBox = sender as type AGInputBox
       if wnd::contentControl::Content instance of type FieldBase *> debugger arrives here
           declare textField as type FieldBase
           set textField = wnd::contentControl::Content as type FieldBase
           attach method wnd::OnFieldTemplateApplied to textField::TemplateApplied
       end-if
   end method.

The VS debugger shows that the ContentControl's Content is null, but then the window is correctly visualized, and maybe its content is set later... It is also null in: - Loaded Event of the Window - ContentRendered Event of the Window And I can't set a Loaded RoutedEventHandler inside the DataTemplate Control, neither with Loaded="OnFieldLoaded" nor with Style + EventSetter, because it's forbidden and won't compile (even if the compiler error suggests to use the EventSetter :/).

Edit: I tried l33t's solution but unfortunately OnContentChanges is never getting called, even if the content is correctly set. I created this class:

   class-id ClassLibraryNew.Controls.NotifyingContentControl public
       inherits type ContentControl.

   01  ContentChanged type EventHandler event public.

   method-id new public.
   procedure division.
       invoke super::new()
   end method.

   method-id OnContentChanged protected override.
   procedure division using by value oldContent as object, by value newContent as object.
       invoke super::OnContentChanged(oldContent, newContent) *> I put a debugger breakpoint here but it's not getting hit
       invoke RaiseContentChanged()
   end method.

   method-id RaiseContentChanged private.
   procedure division.
       declare handler as type EventHandler = ContentChanged
       declare e as type EventArgs = new EventArgs()
       if handler not = null
           invoke run handler(by value self, e)
       end-if
   end method.

   end class.
2

There are 2 answers

0
mm8 On BEST ANSWER

Define the DataTemplates as resources and handle the Loaded event of the TextField, IntegerField and DecimalField root elements:

<ContentControl Grid.Row="1" x:Name="contentControl" >
    <ContentControl.Resources>
        <DataTemplate x:Key="tfTemplate">
            <ctrl:TextField ... Loaded="LoadedHandler"/>
        </DataTemplate>
        <!-- + DataTemplates for IntegerField and DecimalField -->
    </ContentControl.Resources>
    <ContentControl.Style>
        <Style TargetType="{x:Type ContentControl}">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <TextBlock Foreground="Red" Text="Errore: input type non valido. Contattare l'assistenza tecnica."/>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <DataTrigger Binding="{Binding InputType}" Value="Text">
                    <Setter Property="ContentTemplate" Value="{StaticResource tfTemplate}" />
                </DataTrigger>
                <DataTrigger Binding="{Binding InputType}" Value="Integer">
                    <Setter Property="ContentTemplate" Value="{StaticResource ifTemplate}" />
                </DataTrigger>
                ...
            </Style.Triggers>
        </Style>
    </ContentControl.Style>
</ContentControl>
1
l33t On

Even if you manage to determine the value of the content, there is no guarantee that this is the value you see in the UI.

You can try this:

public class ContentControlEx : ContentControl
{
    protected override void OnContentChanged(object oldContent, object newContent)
    {
        // Do stuff...
        base.OnContentChanged(oldContent, newContent);
    }
}

Then use ContentControlEx in place of the regular one.