Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Generic types in WinUI XAML #931

Open
weitzhandler opened this issue Jun 22, 2019 · 18 comments
Open

Proposal: Generic types in WinUI XAML #931

weitzhandler opened this issue Jun 22, 2019 · 18 comments
Labels
area-Parser feature proposal New feature proposal team-Markup Issue for the Markup team wct

Comments

@weitzhandler
Copy link
Contributor

weitzhandler commented Jun 22, 2019

Proposal: Enable generic types in UWP XAML

Currently it's impossible to inherit from a generic type and specify generic type arguments in UWP XAML.

Summary

Elsewhere / API:

public class ReactivePage<TViewModel> : Page, IViewFor<TViewModel> where TViewModel : class
{
  /* ... */
}

XAML:

<rxui:ReactivePage
    x:Class="v:MainPage"
    x:TypeArguments="vm:MainPageViewModel"
    xmlns:rxui="using:ReactiveUI"
    xmlns:v="using:MyProject.Views"
    xmlns:vm="using:MyProject.ViewModels" />

Code behind:

public class MainPage : ReactivePage<MainPageViewModel>
{
   /* ... */
}

Rationale

The WPF XAML supports the TypeArguments XAML directive. My request here is to support this functionality in UWP XAML as well.

P.S. Imigrated from here

@weitzhandler weitzhandler added the feature proposal New feature proposal label Jun 22, 2019
@msft-github-bot msft-github-bot added this to Freezer in Feature tracking Jun 22, 2019
@msft-github-bot msft-github-bot added this to Needs triage in Controls Triage Jun 22, 2019
@jevansaks
Copy link
Member

This is a great request, one that I've talked to @MikeHillberg about in the past. I think the hangup is that it's not clear what the syntax for it should be in XAML markup.

@jevansaks jevansaks added this to Needs triage in Framework Triage via automation Jun 26, 2019
@jevansaks jevansaks removed this from Needs triage in Controls Triage Jun 26, 2019
@jevansaks jevansaks added this to Needs triage in Markup Triage via automation Jun 26, 2019
@jevansaks jevansaks removed this from Needs triage in Framework Triage Jun 26, 2019
@MikeHillberg
Copy link
Contributor

I think it's a good idea. Xaml markup mostly aligns with WinRT, which doesn't generally support generic (parameterized) types. But I don't see a problem with the markup language understanding the places where C# has additional features.

@weitzhandler
Copy link
Contributor Author

weitzhandler commented Jul 1, 2019

This feature is really essential when using ReactiveUI.
See example in my post and RxUI docs.
Because this feature is missing, RxUI users are required to create a dummy class for any reactive view in the project.

@jevansaks, why not same as in the WPF XAML?

@Sergio0694
Copy link
Member

Does this proposal also apply for x:DataType in templates where compiled bindings are used?

Simple example

Model:

public class Box<T>
{
    public T Value { get; set; }
}

XAML:

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:types="using:MySampleTypes"

<DataTemplate x:DataType="types:Box" x:TypeArguments="x:String">
    <TextBlock Text="{x:Bind Value}"/>
</DataTemplate>
One with also function bindings

Model:

public class Box<T>
{
    public T Value { get; set; }
}

Function:

public static class Formatters
{
    public static string FancyFormat(int n) => $"FooBar{n}";
}

XAML:

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:types="using:MySampleTypes"
xmlns:functions="using:MySampleFunctions"

<DataTemplate x:DataType="types:Box" x:TypeArguments="x:Int32">
    <TextBlock Text="{x:Bind functions:Formatters.FancyFormat(Value)}"/>
</DataTemplate>

The workaround to this for now is to always declare an empty class that is just an extension of the generic type, with specific strongly typed arguments. This is not ideal though, for two reasons:

  1. It makes the code way more verbose, and with unnecessary types
  2. It makes it impossible to have generic types which are sealed, because otherwise you won't be able to use point 1. to work around the issue. This forces you to remove that access modifier if you have it, but that's not ideal on a software architecture level.

Another possible workaround is to expose the generic parameters through an additional object property (possibly through an interface), but again this has another set of downsides:

  1. As before, additional verbosity and unnecessary interface types, or properties
  2. Additional casting needed to get back to the actual types
  3. Obligatory boxing if the generic types were value types

P. S. On a related note to the second example, it'd also be nice to have all the primitive types available from the x namespace. I could personally really use x:UInt16 or x:Char.

@MikeHillberg
Copy link
Contributor

For x:DataType we could also use parens to get the language keyword into one attribute:

x:DataType="types:Box(x:String)"

The confusing thing about the following is that it looks like it's instantiating a DataTemplate<string>

<DataTemplate x:DataType="types:Box" x:TypeArguments="x:String">

@MikeHillberg MikeHillberg removed their assignment Feb 14, 2020
@Sergio0694
Copy link
Member

@MikeHillberg That syntax with parents looks great to me! 😄

I just have a question regarding the difference in the syntax for the type arguments in data templates compared to other cases (eg. the MainPage<T> example in the first post). I personally don't mind the difference, but I'm wondering whether it'd be possible to always enable that parens syntax at this point, to have a uniform syntax that doesn't rely on the use case scenario.

If that was possible, we'd end up with the following syntax (again with the code from the first post):

<rxui:ReactivePage
    x:Class="v:MainPage(vm:MainPageViewModel)"
    xmlns:rxui="using:ReactiveUI"
    xmlns:v="using:MyProject.Views"
    xmlns:vm="using:MyProject.ViewModels" />

I personally like that very much, I'm looking forward to seeing how things move forward with this! 😊

@MikeHillberg
Copy link
Contributor

Good question. I think the spec on x:TypeArguments is that it closes the open type indicated by the property element tag (<ReactivePage>), which is what we're doing in the x:Class case. Put another way, use x:TypeArguments to close ReactivePage whether you're creating one or deriving from one.

(I never loved the x:TypeArguments name. It's exactly the right technical term, but I thought x:Of would be easier to understand.)

@Sergio0694
Copy link
Member

Oh I see now, I was reading that wrong 😄

The distinction between x:TypeArguments in that ReactivePage<TViewModel example and the parens syntax for data templates makes perfect sense now, thank you for explaining that!

@michael-hawker
Copy link
Collaborator

@MikeHillberg Does it make sense to use parenthesis which could get confused for functions or use something like curly braces?

<DataTemplate x:DataType="types:Box{x:String}">
    <TextBlock Text="{x:Bind Value}"/>
</DataTemplate>

This would align with xmldoc usage and be more familiar and distinct?

    /// <summary>
    /// An observable group.
    /// It associates a <see cref="Key"/> to an <see cref="ObservableCollection{T}"/>.
    /// </summary>
    /// <typeparam name="TKey">The type of the group key.</typeparam>
    /// <typeparam name="TValue">The type of the items in the collection.</typeparam>

@MikeHillberg
Copy link
Contributor

That looks good to me.

Nothing seems to be perfect, the curly braces look a little like a markup extension (but it's OK because this isn't in column zero), and square brackets looks like an array reference.

@Sergio0694
Copy link
Member

Sergio0694 commented Apr 16, 2020

@MikeHillberg posting here since it's still relevant for generic types in XAML in general, would this issue also cover the proper overload resolution of generic methods used in x:Bind paths? Right now they *kinda* work but you need some tricks to make the code compile.

For instance, suppose I have this property in my viewmodel:

public Task<string> MyTask { get; set; } = Task.FromResult("Hello world!");

And then I have this extensions class:

public static class TaskExtensions
{
    public static object GetResultOrDefault(this Task task)
    {
        throw new NotImplementedException("Use the generic overload");
    }

    public static T GetResultOrDefault<T>(this Task<T> task)
    {
        return task.IsCompletedSuccessfully ? task.Result : default;
    }
}

Now suppose I try to bind to that MyTask property through those extensions:

<TextBlock
    xmlns:ex="using:Extensions"
    Text="{x:Bind ex:TaskExtensions.GetResultOrDefault(ViewModel.MyTask)}"/>

This will work, but the XAML compiler will create this in .g.cs:

global::System.Threading.Tasks.Task<global::System.String> p0;
if (!TryGet_ViewModel_MyTask(out p0)) { return; }
global::System.Object result = global::Extensions.TaskExtensions.GetResultOrDefault(p0);

This is because it'll look up the overload just by literally picking the first one it finds (which is the non-generic one), then it'll declare the return type based of that (object in this case), and still call the generic overload though since the syntax is the same and the C# compiler will prefer that one instead. So you end up with a "hybrid" thing that basically calls the second one and always returns the result as an object. This is the only way to make this work currently, as:

  • Only declaring the generic method fails to build
  • Only declaring the non-generic method means you can't access the Task<T>.Result property

It'd be nice if the XAML compiler was also update to fix this issue.
If this is out of scope for this particular issue, please let me know what's the right place for me to forward this issue and I'll be happy to relay this message there! 😄

Thanks again for your time!

EDIT: just noticed that I can also just add those two methods as instance methods in the viewmodel class, and invoke them directly (since function binding supports multiple paths) like so:

<TextBlock Text="{x:Bind ViewModel.GetResultOrDefault(ViewModel.MyTask)}"/>

Easier to write as there's no need to also declare the xmlns for the extensions class, though it still has the same issue as above with the overload resolution and is also not really ideal for the code organization, as those two methods should really be in a separate extension class, and be static.

@sharpninja
Copy link

var result = global::Extensions.TaskExtensions.GetResultOrDefault(p0);

Solved. How the CS compiler will see that p0 is typed and use the correct overload and result will be of T.

@Sergio0694
Copy link
Member

Sergio0694 commented Apr 16, 2020

@sharpninja Yes, I know that, I've mentioned that in my post as well:

[...] and still call the generic overload though since the syntax is the same and the C# compiler will prefer that one instead. So you end up with a "hybrid" thing that basically calls the second one and always returns the result as an object.

But this is not a "solution" as it still has a number of problems:

  • It causes boxing for value types
  • It makes it impossible to leverage the automatic ToString conversion that the XAML compiler would use when eg. binding an int to a string property like the TextBlock - in this case the codegen would just emit a (string) cast on the boxed int, which would crash at runtime with an InvalidCastException. On the other hand, users would expect that to work as usual instead.
  • It forces you to have that duplicate "obsolete" API in the codebase
  • Users might accidentally try to bind to a Task, which would build but then crash at runtime, defeating one of the whole points of having compiled bindings in the first place

So I wouldn't really say that's "solved" until the XAML compiler gets an update to support this scenario. This is just a workaround, which unfortunately still has a number of downsides.

@MikeHillberg
Copy link
Contributor

This looks worth a separate issue. Changing MyTask to be Task<int> rather than Task<string> makes it more obvious by causing a crash on the type cast.

@paulovila
Copy link

It's also useful for a simple scenario such as using x:Bind within the ListBox.ItemTemplate.
where DataTemplate.DataType is generic. One should be able to declare the generic type and its parameters, with whatever syntax you guys decide, but then, within the template it should be possible to use x:Bind syntax in order to bind to the properties of the type.

@artemious7
Copy link

Want to see this implemented

@lukedukeus
Copy link

+1, right now I am just making an inherited class for each type I need, would be nice to use x:TypeArguments instead

@krschau krschau changed the title Proposal: Generic types in UWP XAML Proposal: Generic types in WinUI XAML Jun 22, 2022
@sylveon
Copy link
Contributor

sylveon commented Jun 22, 2022

Any chance this could also support C++ templates?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Parser feature proposal New feature proposal team-Markup Issue for the Markup team wct
Projects
Development

No branches or pull requests