Closed scottcg closed 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.
@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.
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!
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.
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?
@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
@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.
@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;
}
}
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!
@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.
That's great. I'm looking forward to trying it out. Thanks
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)
@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!
@scottcg I just added all remaining helpers only this todo:
NJoy playing around with it!
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?
Hi @scottcg, I changed the namespace from System.Windows.Markup
to CSharpMarkup.Wpf
for a couple of reasons:
DependencyObject
in System.Windows
, so having a System.Windows.Markup.DependencyObject
results in ambiguity even if a file only has a using System.Windows.Markup
.Microsoft.UI.Xaml.DependencyObject
which will not be in scope when using Microsoft.UI.Markup;
Xamarin.Forms.Markup
namespace and I carried that over to C# Markup 2 for WinUI. However, after C# Markup 1 was moved to the Xamarin Community Toolkit as part of the transition to Maui, and later ported to the Maui Community Toolkit, the namespace was required to be in the toolkit namespace: Xamarin.CommunityToolkit.Markup
and CommunityToolkit.Maui.Markup
. To be prepared for C# Markup 2 being incorporated in toolkits like this, I ensure that C# Markup 2 works as developer friendly as possible without depending on being in the UI framework's namespace. So if this works out well for WPF I will change it for WinUI as well.Infragistics.WinUI.Gauges
with that same namespace, the C# Markup support NuGet could use CSharpMarkup.WinUI.Infragistics.Gauges
.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:
Assign
and Invoke
methods as documented here.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.
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)
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?
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
toCSharpMarkup.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
andDataTemplate
, howeverDataTemplate
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 thatDataTemplate
.
Closed the issue - WPF is now code complete - but not tested - for initial version
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.
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
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!