VincentH-Net / CSharpForMarkup

Concise, declarative C# UI markup for .NET browser / native UI frameworks
MIT License
748 stars 43 forks source link

Support WPF #15

Closed scottcg closed 2 years ago

scottcg commented 2 years ago

Would be great to see the generators included in the source, maybe they are there and I'm just missing them.

I've been using similar hand-coded techniques for WPF (bunch of helper/extension classes) - but I don't want to maintain that any longer.

Thanks!

VincentH-Net commented 2 years ago

@scottcg I would be interested to learn from what you have built for WPF - is that public? What is the biggest application it was used for?

The generators are not public yet, for now I am focusing on getting the shape, functionality and performance of the generated API to production quality on all WinUI + UNO platforms.

Next comes support for main 3rd party libraries, and then possibly more UI frameworks like MAUI or Blazor. WPF could also be one.

My vision for the codegen is to get it to a point where it will generate the API on the fly for any controls you build or consume in your app as an app developer, in the main .NET IDE's and for the main .NET UI frameworks. But first things first.

scottcg commented 2 years ago

@VincentH-Net no it's not public - I'm updating an app to .NET 6 and it was done using limited XAML; it's about 500 KLOC. The code looks similar to what you have though all styles are still done in XAML. Also it has a lot of Telerik as well.

I've taken a couple of hours and hand edited the WinUI generated file and other source to get basic WPF going; it's mostly a war of namespaces at this point. So far I like what you have.

I think it would be worth while to have WPF support early in your library - I'd be happy to contribute a port if that is something you are interested in.

Thanks.

VincentH-Net commented 2 years ago

That is interesting @scottcg; I wonder how many WPF devs would prefer C# UI to XAML and actually use it in WPF? Do you know of any other devs? I expected WPF apps were all built in XAML due to the 1st class tooling support, and not much new apps /UI are going to be built in WPF.

I will check if the codegen can handle WPF without too much work. If it can, let's ask around if more devs would be interested. If there is a reasonable number (in absolute terms) then I think it would be worth it to add support, if you could contribute a port of the manual API's?

You up for that? Thanks!

scottcg commented 2 years ago

I've been doing WPF for a long time (>13 years); XAML in near old days, imho never achieved the benefits (remember for a long time there was nothing like Intellisense, x:Bind, bind debugging, no hot reload, Blend codegen!.... on and on).

If you think the WinUI devs would be attracted to what you are doing, I can see no reason why WPF devs wouldn't see the benefits - the experience is almost the same. I've been tinkering with WinUI for desktop and I don't see it as a replacement for WPF for at least another year.

I'm happy to contribute whatever code I have, let me bang on the code this weekend and see where I get.

jpmikkers commented 2 years ago

As someone who would like to migrate/rewrite some winforms UI Apps to a more modern look, this code-first gui building looks amazing (apologies for being off-topic a bit). Vincent, would you still need code-generators if you only targeted a single api, winui3 for example?

VincentH-Net commented 2 years ago

@scottcg

I'm happy to contribute whatever code I have, let me bang on the code this weekend and see where I get.

Thanks, best if you could focus on the manual api's - everything except the .generated.cs files.

I will try to generate those .generated.cs fully from the wpf binary so manual edits in that would be lost anyway

Also fine by me if you want to wait for the generated code before you take on the manual apis. I think that would save you effort

VincentH-Net commented 2 years ago

@jpmikkers

would you still need code-generators if you only targeted a single api, winui3 for example?

Yes - to maintain the nuget on WinUI framework updates, and to support 3rd party ui libraries or your own custom controls.

If you just use the current nuget for a windows desktop target for windowsappsdk 1.0, you dont need codegen.

scottcg commented 2 years ago

@VincentH-Net I have a version that compiles and mostly runs! Other than the namespace changes, the biggest problem I ran into is the class hierarchy in WPF is a bit different; there are several places where a control is derived from DispatcherObject (WPF) vs DependencyObject (Style, Brush, SolidColorBrush, Border...). Those problems are almost all inside the generated code so I just did a fast swap and moved on. I also skipped most of the extensions classes in the codegen - except where I wanted something so I could create a small example.

I also have all of the hand built code converted, again almost all of this is swapping to WPF namespaces and resolving method conflicts (remove/add). I think most of this can be resolved with some conditionals and #if's.

I'm having various binding problems, not sure why... but I'll work on the tomorrow. Plus I need to build a larger example to flesh this out.

Here's what I did for IUI to solve the DispatcherObject issue:

namespace System.Windows.Markup // DataTemplate
{
    using Xaml = System.Windows;
    public static partial class Helpers
    {
        /// <summary>Create a <see cref="Xaml.DataTemplate"/></summary>
        public static DataTemplate DataTemplate()
        {
            var ui = new Xaml.DataTemplate();
            return System.Windows.Markup.DataTemplate.StartChain(ui);
        }
    }

    public interface IUI_D<TUI> where TUI : System.Windows.Threading.DispatcherObject
    {
        TUI UI { get; }
    }

    public partial class DataTemplate : IUI_D<System.Windows.DataTemplate>
    {
        static DataTemplate instance;

        internal static DataTemplate StartChain(Xaml.DataTemplate ui)
        {
            if (instance == null) instance = new DataTemplate();
            instance.UI = ui;
            return instance;
        }

        Xaml.DataTemplate ui;

        public new Xaml.DataTemplate UI
        {
            get => ui;
            protected set => ui = value;
        }

        public static implicit operator Xaml.DataTemplate(DataTemplate view) => view?.UI;

        public static implicit operator DataTemplate(Xaml.DataTemplate ui) => DataTemplate.StartChain(ui);

        protected DataTemplate() { }
    }
}

This illustrates what I've done (System.Windows vs. System.Windows.Markup etc.):

namespace System.Windows.Markup
{
    using Xaml = System.Windows;

    public static partial class GridExtensions
    {
        public static TTarget Grid_Location<TTarget>(this TTarget target, int row, int column) where TTarget : FrameworkElement
        {
            Xaml.Controls.Grid.SetRow(target.UI, row);
            Xaml.Controls.Grid.SetColumn(target.UI, column);
            return target;
        }
    }
}

Here's a sample I've been using:

namespace WpfSampleApp
{
    public partial class ShellWindow
    {
        public void Build() => Content =
            Border(
                Grid(
                    Rows(Star, Star), Columns(Star, Star),
                    HStack(
                        TextBlock()
                        .Foreground("black")
                        .FontSize(30)
                     ).Background("yellow").Grid_Row(0).Grid_Column(0),
                    TextBlock("hi @ 0,1").Foreground("black").FontSize(30).Grid_Row(0).Grid_Column(1),
                    Button("Button")
                        .Foreground("Green")
                        .FontSize(24)
                        .Padding(50)
                        .Grid_Row(1)
                        .Grid_Column(0) ,
                    HStack(
                            TextBlock(ViewModel.Title).Foreground("black").FontSize(30)
                        ).Background("pink").Grid(Row: 1, Column: 1)
                    )
                    .Background(SolidColorBrush(System.Windows.Media.Colors.Blue)
                )
            )
            .Padding(0)
            .BorderBrush(System.Windows.Media.Colors.Red)
            .BorderThickness(5)
            .Margin(new System.Windows.Thickness(10))
            .DataContext(ViewModel)
            .UI;
    }
}
VincentH-Net commented 2 years ago

Nice progress @scottcg ! I spent half of today to adjust codegen to Wpf, close to doing a first run. Incorporating your DispatcherObject fix still todo. If I get Wpf codegen working, I will add the generated code to Wpf branch so you can work in there in a fork and create a PR.

Btw the issue title does not cover the content any more; could you change it to "Support WPF" ? Thanks!

VincentH-Net commented 2 years ago

@scottcg I made progress - first WPF codegen compiles and contains 600+ markup objects. I can create simple markup OK, will check tomorrow if codegen for advanced markup is correct (enums, attached properties, templates, binding etc). Will keep you posted.

scottcg commented 2 years ago

That's great. I'm looking forward to trying it out. Thanks

VincentH-Net commented 2 years ago

I fixed WPF binding, attached properties, enum valued properties and more. I only need to resolve the DispatcherObject issue for the relevant types.

@scottcg I have a question: based on your experience, which (if any) of the types that derive from DispatcherObject but not from DependencyObject - see the list in this file - should be surfaced as markup element build helpers?

E.g. DataTemplate() helper is needed so you can pass in a lambda to views that take DataTemplate typed properties. Any more types from above file that should have similar helpers? (btw I generated the file just to list the candidate types for this question - that code won't compile)

scottcg commented 2 years ago

@VincentH-Net Good question - most of what's in the file (bitmap encoders for example) I've never used in markup. You have the important bits like templates and styles.

Let me grab the code and try it out!

VincentH-Net commented 2 years ago

@scottcg I just added all remaining helpers only this todo: image

NJoy playing around with it!

scottcg commented 2 years ago

Quick question/observation, you moved this to your own namespace, what's your thinking about this? I wondered why you hadn't done so in your other implementations - but eventually I bought into the ease of the experience by leveraging the standard namespaces.

I need to tinker some more but this change makes interop between C# markup and XML/code-behind a bit tricky. For example this is how you have to declare stuff to get this to work (the partial on the class decl causes problems, but it you drop that then you have other problems).

using CSharpMarkup.Wpf;

namespace CSharpMarkup.Example
{
    public partial class MainWindow : System.Windows.Window // full path reqd here
    {
        public MainWindow()
        {
            Content = Helpers.Grid(
                    Helpers.Rows(Helpers.Star),
                    Helpers.Columns(Helpers.Star),
                    Helpers.TextBlock("hello")
                        .Foreground("black")
                        .FontSize(30)
                        .Padding(30)
                ).UI;
        }
    }
}

The alternate has it own issues (if your derive from the your Window, you loose access to System.Windows.Window)

using CSharpMarkup.Wpf;

namespace CSharpMarkup.Example
{
    public class ShellWindow : Window
    {
        public ShellWindow()
        {
            var content = Helpers.Grid(
                    Helpers.Rows(Helpers.Star),
                    Helpers.Columns(Helpers.Star),
                    Helpers.TextBlock("hello")
                        .Foreground("black")
                        .FontSize(30)
                        .Padding(30)
                ).UI;

            UI.Content = content;
        }
    }
}

Am I missing something?

VincentH-Net commented 2 years ago

Hi @scottcg, I changed the namespace from System.Windows.Markup to CSharpMarkup.Wpf for a couple of reasons:

C# Markup is designed to separate UI markup from UI logic while using the same class/helper names, so the markup can read very similar to what people are used to, while avoiding adding noise in the names. To prevent ambiguities between markup and ui types and helper methods, the approach is not to use the markup namespace and the ui framework namespaces in the same file.

Instead:

Note that Assign and Invoke pass the UI objects, not the markup objects, to support above separation. Also note that the markup types are not safe to use outside a markup expression; they are optimized to prevent garbage collector pressure (only one instance of each markup type exists, there is zero object creation overhead - only the UI objects are created).

To illustrate this approach, I applied it to your example: NamespaceExample.cs:

using CSharpMarkup.Wpf;
using static CSharpMarkup.Wpf.Helpers;
using static System.Windows.Media.Colors;

namespace WpfApp1;

partial class NamespaceExample
{
    void Build() => Content =

    Grid (
        Rows (Star), Columns (Star),

        VStack (
            TextBlock ("Hello")
                .FontSize (30) .Foreground (Black),

            TextBlock ("Expose this to .logic.cs")
                .FontSize (30) .Foreground (Black)
                .Assign (out myTextBlock),

            TextBox (Text: "Invoke logic on this")
                .FontSize (30) .Foreground (Black)
                .Invoke<TextBox, System.Windows.Controls.TextBox>(InitializeMyTextBox)
        )
    )

    .UI;
}

NamespaceExample.logic.cs:

using System.Windows;
using System.Windows.Controls;

namespace WpfApp1;

partial class NamespaceExample : Window
{
    TextBlock myTextBlock;

    public NamespaceExample()
    {
        Build();
    }

    void InitializeMyTextBox(TextBox textBox)
        => textBox.GotFocus += TextBox_GotFocus;

    void TextBox_GotFocus(object sender, RoutedEventArgs e)
        => myTextBlock.Text = "Got Focus";
}

Only the Invoke method requires using the full ui namespace because the compiler is not smart enough to infer the type parameters. I will look into solving that, but in any case using Invoke is pretty rare.

Pls let me know if/how this approach works for you, thanks!

Btw I intend to make this pattern even more easy by publishing item templates for C# markup windows / pages that create both files.

VincentH-Net commented 2 years ago

I investigated how to avoid specifying the UI namespace when calling Invoke, but the only way I found would potentially change the type of the fluent call chain to a base type.

You can reduce the noise however:

using UI = System.Windows.Controls;
// ...
TextBox (Text: "Invoke logic on this")
    .Invoke<TextBox, UI.TextBox>(InitializeMyTextBox)
scottcg commented 2 years ago

Thanks for the detailed explanation! Do you think you'll split our WinUI as well?

I think having this separated makes a lot of sense, let me go back to my example project and deepen my usage of the code.

I think I've found a larger and nearly complete application that I can convert and open source.

Do you think we should close this issue and deal with future issues separately?

VincentH-Net commented 2 years ago

Thanks for the detailed explanation! Appreciated! I like these types of questions, they help me validate my thinking.

Do you think you'll split our WinUI as well? If by splitting WinUI you mean moving Microsoft.UI.Markup to CSharpMarkup.WinUI namespace then yes. Depending on if it works for you in WPF.

I think I've found a larger and nearly complete application that I can convert and open source. It would be great if you can use CSharpMarkup.Wpf in a real-world app; it will validate choices and you may come up with improvements / additions based on your long WPF experience.

Do you think we should close this issue and deal with future issues separately? Definitely! I just pushed support for DispatcherObjects Style and DataTemplate, however DataTemplate does not show yet. I will add an issue and ask you to look into it, something with the Xaml I am trying to load into that DataTemplate.

VincentH-Net commented 2 years ago

Closed the issue - WPF is now code complete - but not tested - for initial version

VincentH-Net commented 2 years ago

Hi @scottcg, FYI I pushed fixes a few days ago - DataTemplate() and the ListView helper that uses it are now working in WPF, e.g.:

            ListView (() =>
                TextBlock().Bind()
                .FontSize(30).Foreground(Black)
            )   .Bind(vm.Items) 

PS maybe it's handy to connect on discord / twitter / email? Let me know.

VincentH-Net commented 2 years ago

Hi @scottcg , np time off tech is healthy.

Btw for discord a name+tag is needed, could you connect to mine? It is

VincentH.NET#7658

Or if you prefer, my twitter handle is

@VincentH_NET

(the twitter handle you mentioned seems to be someone else)

Thanks