0

I'm testing an implementation of an application I need to write in WPF (.NET 4.6.2). The application will have a list of messages. When a message from the list is selected, the details of that message will be presented in a separate area. The messages can be of different types, so the format of the details section will vary. Pretty straight forward master-detail layout stuff.

The unfortunate part is that due to the middleware we're using, the DLL that contains the definitions for the message classes defines fields, not properties, for the members of the classes. My challenge is to update the details section of the UI without being able to rely on binding.

To test this out, I've written an application that has a ListBox and ContentControl, for displaying the selected message details. I have a simple Message class, that has an Id property. For the UI, I've created two DataTemplates that can be selected from to display the message details, depending on whether the current Message has an Id value of 0 or not.

The XAML for the layout is:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:TestGui"
        x:Name="window" x:Class="TestGui.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <local:MessageDetailSelector x:Key="MessageDetailSelector" />
        <local:MessageList x:Key="MessagesList">
            <local:Message/>
            <local:Message Id="1"/>
            <local:Message Id="2"/>
        </local:MessageList>
        <DataTemplate x:Key="ZeroDetailTemplate">
            <StackPanel Orientation="Horizontal">
                <Label>Zero Detail: </Label>
                <Label x:Name="DetailLabel" />
            </StackPanel>
        </DataTemplate>
        <DataTemplate x:Key="DetailTemplate">
            <StackPanel Orientation="Horizontal">
                <Label>Detail:</Label>
                <Label x:Name="DetailLabel" "/>
            </StackPanel>
        </DataTemplate>
    </Window.Resources>
    <DockPanel>
        <ListBox x:Name="listBox" DockPanel.Dock="Left" MinWidth="150" ItemsSource="{Binding Source={StaticResource MessagesList}}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label Content="{Binding Id}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <local:CustomContentControl x:Name="DetailControl" Content="{Binding SelectedItem, ElementName=listBox}" ContentTemplateSelector="{StaticResource MessageDetailSelector}"/>
    </DockPanel>
</Window>

...and the code:

using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;

namespace TestGui
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();            
        }

        // Can be used to programmaticaly set DataTemplate:
        //  - add SelectionChanged="MessageSelectionChanged to <ListBox> in XAML
        //  - comment out ContentTemplateSelector from <local:CustomContentControl>
        private void MessageSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (e.AddedItems.Count > 0 && e.AddedItems[0] is Message msg)
            {
                DetailControl.ContentTemplate = msg.Id == 0 ?
                    FindResource("ZeroDetailTemplate") as DataTemplate :
                    FindResource("DetailTemplate") as DataTemplate;

                DetailControl.Content = msg;
            }
            else
            {
                DetailControl.ContentTemplate = null;
            }
        }
    }

    class CustomContentControl : ContentControl
    {
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            ApplyTemplate();
            var cp = VisualTreeHelper.GetChild(this, 0) as ContentPresenter;
            System.Diagnostics.Debug.Assert(cp != null);

            DataTemplate dt = cp.ContentTemplate;
            Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
                (Action)(() =>
                {
                    try
                    {
                        dt.FindName("DetailLabel", this);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnContentChanged: Cannot find DetailLabel\n{e.Message}");
                    }
                }));
        }

        protected override void OnContentTemplateChanged(DataTemplate oldContentTemplate, DataTemplate newContentTemplate)
        {
            base.OnContentTemplateChanged(oldContentTemplate, newContentTemplate);

            ApplyTemplate();
            var cp = VisualTreeHelper.GetChild(this, 0) as ContentPresenter;
            System.Diagnostics.Debug.Assert(cp != null);

            DataTemplate dt = cp.ContentTemplate;
            Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
                (Action)(() =>
                {
                    try
                    {
                        dt.FindName("DetailLabel", this);
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"OnContentTemplateChanged: Cannot find DetailLabel\n{e.Message}");
                    }
                }));
        }
    }

    class Message
    {
        public int Id { get; set; } = 0;
    }

    class MessageList : ObservableCollection<Message> { }

    class MessageDetailSelector : DataTemplateSelector
    {
        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            if (container is FrameworkElement element && item is Message message)
            {
                if (message.Id == 0)
                {
                    return element.FindResource("ZeroDetailTemplate") as DataTemplate;
                }
                else
                {
                    return element.FindResource("DetailTemplate") as DataTemplate;
                }
            }

            return null;
        }
    }
}

For the most part, it works as expected. I can select one of the messages from the ListBox, and the correct template is displayed depending on the id. However, I now need to be able to manually update the values of the controls in the DataTemplate.

I created the CustomContentControl class so I could override the methods of the ContentControl and try to figure out where I can intercept the template to update it. According to this MSDN article, I should be able to call FindName on the ContentPresenter.

As implemented in this example, using the MessageDetailSelector to automatically select the template, the OnContentChanged method is being called. However, this is causing an exception because the ContentTemplate reference is null in this line:

DataTemplate dt = cp.ContentTemplate;

I tried removing the ContentTemplateSelector attribute from the <local:CustomContentControl> element, and setting SelectionChanged attribute of the ListBox to my MessageSelectionChanged method. That causes an InvalidOperationException, which lead me to this SO question.

I've tried calling ApplyTemplate (which is returning false btw), as well as using Dispatcher.BeginInvoke, but I'm still getting the same exception.

Any ideas on what I can do get a hold of the DataTemplate controls properly, or even thoughts on another method for updating the controls without binding would be appreciated.

TWReever
  • 357
  • 4
  • 14
  • 1
    Just an idea, but instead of dealing with DataTemplates, you may write a Binding Converter (for the SelectedItem Binding) that returns the complete UI element that visualizes an item. – Clemens Aug 15 '18 at 14:02
  • 1
    Or you write a Binding Converter that returns a wrapper class with properties that makes it usable as Binding source object. DataTemplates aren't particular useful without the possibility to data bind their elements. – Clemens Aug 15 '18 at 14:06
  • @Clemens - I have thought about writing wrapper classes. Unfortunately, there are more than a few message classes, and they're pretty complex and potentially fluid until the design is fully locked down. I dreamed of a way to dynamically generate the wrappers, but I haven't motivated myself enough to go down that path yet :). I will look into your suggestion on Binding Coverters in the meantime. – TWReever Aug 15 '18 at 14:10

0 Answers0