User avatar
BenJo
Topic Author
Posts: 12
Joined: 20 Jun 2024, 15:42

Drive ContentControl DataTemplate from a model data string

21 Oct 2024, 12:01

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.
 
User avatar
sfernandez
Site Admin
Posts: 3289
Joined: 22 Dec 2011, 19:20

Re: Drive ContentControl DataTemplate from a model data string

21 Oct 2024, 13:52

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>();
  //...
}
 
User avatar
BenJo
Topic Author
Posts: 12
Joined: 20 Jun 2024, 15:42

Re: Drive ContentControl DataTemplate from a model data string

21 Oct 2024, 17:02

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.
 
User avatar
sfernandez
Site Admin
Posts: 3289
Joined: 22 Dec 2011, 19:20

Re: Drive ContentControl DataTemplate from a model data string

21 Oct 2024, 18:38

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.
 
User avatar
BenJo
Topic Author
Posts: 12
Joined: 20 Jun 2024, 15:42

Re: Drive ContentControl DataTemplate from a model data string

21 Oct 2024, 19:40

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.
 
User avatar
sfernandez
Site Admin
Posts: 3289
Joined: 22 Dec 2011, 19:20

Re: Drive ContentControl DataTemplate from a model data string

24 Oct 2024, 09:56

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.

Who is online

Users browsing this forum: No registered users and 4 guests