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 DataTemplate
s 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.