Page 1 of 1

Drive ContentControl DataTemplate from a model data string

Posted: 21 Oct 2024, 12:01
by BenJo
Hey, I wanted to create a menu screen system, which shows the content for a screen based on that screen being the current one. Trailing NoesisGUI-UE5.3-3.2.4 Unreal Plug-in.

I've seen this forum posts describing the 3 methods of doing similar are: using visibility (Menu3D), ContentControl's & Data Templates (LayeredUI), or manually loading the xaml. I've proved each works ok, but I'd prefer to only load elements when required, and also to do so in a data driven way (so not with a code class that says with xaml it loads).

I've created this setup, and wanted to use model data to define a string (DataTemplateName), to look-up which data template it should show (for now these data templates are just text, but later I imagine they could be complex screens, defined in a separate resource dictionary & xamls).

Xaml:
  <Page.Resources>
    <ResourceDictionary>
      <DataTemplate x:Key="MyDataTemplate">
        <TextBlock Text="MyDataTemplate" FontSize="30" Foreground="#00FFFF"/>
      </DataTemplate>
      <DataTemplate x:Key="MyDataTemplate2">
        <TextBlock Text="MyDataTemplate2" FontSize="30" Foreground="#00FFFF"/>
      </DataTemplate>

      <local:UserControlTemplateConverter x:Key="ControlConverter" />
    </ResourceDictionary>
  </Page.Resources>

  <Grid>
    <StackPanel>
      <!-- Content Controls with just static resources (all working ok) -->
      <TextBlock Text="Static Resources: " Foreground="Red" />
      <ContentControl ContentTemplate="{StaticResource MyDataTemplate}"/>
      <ContentControl Content="{StaticResource MyDataTemplate}"/>
      <ContentControl ContentTemplate="{StaticResource MyDataTemplate2}"/>
      <ContentControl Content="{StaticResource MyDataTemplate2}"/>

      <TextBlock Text="Model String: " Foreground="Red" />
      <!-- Doesn't work, as trying to use a string as the template. Is there any syntax that can do that? -->
      <ContentControl ContentTemplate="{Binding Path=DataTemplateName}"/>

      <TextBlock Text="Model String using a converter: " Foreground="Red" />
      <!-- I thought this should work, but it doesn't, nothing shows. What's wrong? -->
      <ContentControl ContentTemplate="{Binding Path=DataTemplateName, Converter={StaticResource ControlConverter}}"/>
      <ContentControl Content="{Binding Path=DataTemplateName, Converter={StaticResource ControlConverter}}"/>
 
C++ Model Data with a String:
UCLASS(Blueprintable)
class UNoesisUserControlModelData : public UObject
{
	GENERATED_BODY()

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FString DataTemplateName = TEXT("MyDataTemplate");
My first question is whether it's possible to look-up a template using a string from a model, to match it's key name? Is there any special bind syntax for it? I've not seen a way to do that yet, so I tried making my own converter for it. This all looks to work in code (I see it finds the DataTemplate and returns it), but doesn't show. Do you know what I might be doing wrong? It's probably something simple I've not quite understood about how templates work perhaps?

Converter to convert String -> DataTemplate:
bool TryConvert(Noesis::BaseComponent* value, const Noesis::Type* targetType,
	Noesis::BaseComponent* parameter, Noesis::Ptr<Noesis::BaseComponent>& result) override
{
	Noesis::String TemplateName;
	if (Noesis::Boxing::CanUnbox<Noesis::String>(value)) TemplateName = Noesis::Boxing::Unbox<Noesis::String>(value);
	else return false;

	Noesis::BaseComponent* BaseComponent = FindNodeResource(TemplateName.Str(), true); // I've also tested doing Noesis::DynamicCast<Noesis::DataTemplate*> returns a valid class.

	if (BaseComponent)
	{
		result = Noesis::Boxing::Box(BaseComponent);
		return true;
	}
	return false;
}
Result:
Image

If this approach is completely wrong, please just let me know. My issue with the LayeredUI method, was that it requires a C++ class for the model object and data template to work, which feels a bit excessive & hard-coded, when you start having lots of screens.

Thanks, appreciate any help or feedback.

Re: Drive ContentControl DataTemplate from a model data string

Posted: 21 Oct 2024, 13:52
by sfernandez
Hi,

The approach you followed is perfectly valid. Using a Converter to find the appropriate template to apply to a control is a common practice, for example if you want to generate the resource key based on the value of a property of the model. Another option is to use a DataTemplateSelector, but the implementation would be quite similar.

Regarding the example you posted, I'm not able to reproduce your behavior. Could you add a TextBlock that binds to the model property to see if the data context is correct?
<TextBlock Text="Model String using a converter: " Foreground="Red"/>
<!-- Should show the model DataTemplateName property value -->
<TextBlock Text="{Binding Path=DataTemplateName}"/>
<!-- I thought this should work, but it doesn't, nothing shows. What's wrong? -->
<ContentControl ContentTemplate="{Binding Path=DataTemplateName, Converter={StaticResource ControlConverter}}"/>
Can you also make sure that the converter is registered in the module startup:
virtual void StartupModule() override
{
  Noesis::RegisterComponent<YourNamespace::UserControlTemplateConverter>();
  //...
}

Re: Drive ContentControl DataTemplate from a model data string

Posted: 21 Oct 2024, 17:02
by BenJo
Interesting, that's good that I'm at least on the right track.

The TextBlock correctly outputs "MyDataTemplate" & my register is in the StartupModule of our game. When I breakpoint my convert function is returning the component & true.

<ContentControl Content="{Binding Path=DataTemplateName, Converter={StaticResource ControlConverter}}"/>
Just noticed this shows the text "DataTemplate" too. Sorry, I think in my original screenshot was taken when I was trying to casting it to a Noesis::DataTemplate in code which casts correctly but nothing showed. Either way it just shows the type not the template contents (maybe it's a clue?)


Are you reproducing in the Unreal solution, and it shows ok? I can try recreate it in the Vanilla UE5 Samples Project, to rule out any issues with our engine.

Re: Drive ContentControl DataTemplate from a model data string

Posted: 21 Oct 2024, 18:38
by sfernandez
Yes, I recreated the sample in Unreal using the following converter (I've just added it to the DataBinding module in our Samples project):
bool TemplateConverter::TryConvert(BaseComponent* value, const Type*,
    BaseComponent* parameter, Noesis::Ptr<BaseComponent>& result)
{
    if (Noesis::Boxing::CanUnbox<Noesis::String>(value))
    {
        const char* name = Noesis::Boxing::Unbox<Noesis::String>(value).Str();
        Noesis::BaseComponent* resource = FindNodeResource(name, true);
        if (Noesis::DynamicCast<Noesis::DataTemplate*>(resource) != 0)
        {
            result.Reset(resource);
            return true;
        }
    }

    return false;
}
And xaml:
<Grid
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:DataBinding">
  <Grid.Resources>
    <DataTemplate x:Key="MyDataTemplate">
      <TextBlock Text="This text defines MyDataTemplate" FontSize="30" Foreground="#00FFFF"/>
    </DataTemplate>
    <DataTemplate x:Key="MyDataTemplate2">
      <TextBlock Text="This other text defines MyDataTemplate2" FontSize="30" Foreground="#0080FF"/>
    </DataTemplate>
    <local:TemplateConverter x:Key="TemplateConverter" />
  </Grid.Resources>
  <StackPanel Margin="20">
    <TextBox x:Name="txt" Text="" Margin="0,10,0,0"/>
    <TextBlock Text="{Binding Path=Text, ElementName=txt}" Foreground="White"/>

    <!-- Content Controls with just static resources (all working ok) -->
    <TextBlock Text="Static Resources: " Foreground="Red" Margin="0,20,0,0"/>
    <ContentControl ContentTemplate="{StaticResource MyDataTemplate}"/>
    <ContentControl Content="{StaticResource MyDataTemplate}"/>
    <ContentControl ContentTemplate="{StaticResource MyDataTemplate2}"/>
    <ContentControl Content="{StaticResource MyDataTemplate2}"/>

    <TextBlock Text="Model String using a converter: " Foreground="Red" Margin="0,20,0,0"/>
    <ContentControl ContentTemplate="{Binding Path=Text, ElementName=txt, Converter={StaticResource TemplateConverter}}"/>
    <ContentControl Content="{Binding Path=Text, ElementName=txt, Converter={StaticResource TemplateConverter}}"/>
  </StackPanel>
</Grid>
I used a simple TextBox as the source of the string, instead of a ViewModel, but it should be the same.

Re: Drive ContentControl DataTemplate from a model data string

Posted: 21 Oct 2024, 19:40
by BenJo
I copied just your code for the converter implementation, and that fixed it!

It seems this 1 line in my C++ code was wrong:
// My incorrect code
//result = Noesis::Boxing::Box(resource);
// Working!
result.Reset(resource);
return true;
I presume it's because it's a type that doesn't need boxing to return? (Read up that Boxing is only need for "basic types or structs").

Thank you for taking the time to reproduce & resolve.

Re: Drive ContentControl DataTemplate from a model data string

Posted: 24 Oct 2024, 09:56
by sfernandez
Glad to hear it worked! I'll mark this topic as solved then.

And yes, BaseComponent inherited classes don't need to be boxed, only structs and basic types.