fsprojects / FsXaml

F# Tools for working with XAML Projects
http://fsprojects.github.io/FsXaml/
MIT License
171 stars 48 forks source link

simplifying XAML type provider #15

Closed dmitry-a-morozov closed 10 years ago

dmitry-a-morozov commented 10 years ago

Why XAML Type Provider cannot generate a root type that erases to System.Windows.Window and for every visual element has provided property of appropriate type (Button, TextBox etc)? Design with CreateRoot`` factory method and nestedAccessor``` seems non-intuitive and redundantly complex.

ReedCopsey commented 10 years ago

There are two parts here:

1) Why not erase to Window:

The TP was designed to be a generative type provider, not an erasing one, for many reasons. Unfortunately, this leads to the need for the CreateRoot factory on some types (mainly Window and Application), due to limitations in how the WPF stack handles merging code into an existing type, and the way x:Class definitions happen in XAML.

The reason for the generative provider instead of the erasing provider is actually because the old FSharpX (erasing) method has serious limitations - you can't compose views in XAML if you don't have "real" types. This means it works great for "one screen" demos (ie: a single window), but fails horribly for "real world" applications with custom UserControls, merged resource dictionaries, etc. This requires types to exist and be discoverable at compile time, which in turn means you need a real type. The generative TP provides this.

It might be possible to make Window erasing and all other types generative, but I'm not sure that it really solves the problem anyways - you can't use the type "strongly" at that point, which has its own disadvantages. This also wouldn't necessarily solve anything when used with your library - you still would need to make a new class that implemented the IView<> interfaces appropriately, which wouldn't work with an erased window directly. I'm not convinced that the confusion of having two completely different methods of working (some types erased vs some types generated) wouldn't be worse than the current scenario. Erasing to window and providing properties would effectively be the same as just using the CreateRoot() result and your ? operator, unless I'm missing something...

2) Nested Accessor type:

This one may be possible to change, I'd have to look at it. To be fair, part of this is that I do tend to use MVVM with WPF, which really eliminates the need for this. At this point, I've only used the Accessor type once in production code (out of around 40-50 views in production using the TP), and that was just because I was too lazy to write a behavior for a very odd requirement in one view.

That being said, I do see the issues that would crop up when trying to use it from your library, as you work directly on the controls. I'm not opposed to looking at changing how this works - but I do kind of feel that exposing controls as public properties directly on the class is a flawed design. Even the C# designer doesn't do this... The Accessor was more of my way of trying to at least make it very clear when you're accessing "the internals" of the XAML defined view.

ReedCopsey commented 10 years ago

@dmitry-a-morozov I've been thinking about how to remove or simplify the Accessor requirement, and am open to suggestions.

The problem is, the way WPF's XAML loader works, when I load the XAML at runtime, I get a type of Window or Application directly, which means I can't subclass it. This means there's no way to directly add the properties to the returned type. (That's also, btw, why the CreateRoot() method needs to exist.) The C# designer works around this by using some internal methods which load the resource and convert to the appropriate type, but they, unfortunately, only work on BAML, not XAML, which means they'd be impossible to use without figuring out a way to pre-compile the xaml resource streams.

As such, I end up with a way to load a "Window", and a type that represents something that can load the specific window in question. The Accessor was merely a way to wrap that.

It might be possible to change the design around so that you'd do something along the lines of:


type MainWindow = XAML<"MainWindow.xaml">

let window = MainWindow()  // Construct the "MainWindow" directly
window.SomeTextblock.Content <- "Foo"  // Gets access to named items
window.Root.ShowDialog() // Note ".Root" property retrieves the actual "Window"

UserControls could work similarly - though you'd be able to eliminate the need to use .Root in that case, as my current workaround would still be possible.

Would this suffice for you?

dmitry-a-morozov commented 10 years ago

This should work. A user can leverage exiting helper base view type: https://github.com/fsprojects/FSharp.Desktop.UI/blob/master/src/View.fs#L24


type MainWindow = XAML<"MainWindow.xaml">

type MyView(xaml : MainWindow) =
    inherit View<MyEvents, MyModel>(xaml.Root)
    ...
ReedCopsey commented 10 years ago

mmm - I don't think that would work, actually. MainWindow, in the above, wouldn't subclass Window directly. It seems like it would be fairly easy to make a helper class similar to that one, but it wouldn't work directly as you wrote it, would it?

dmitry-a-morozov commented 10 years ago

Root property passed to the base View subtypes Window. That's good enough.

ReedCopsey commented 10 years ago

@dmitry-a-morozov Just published a new version that will hopefully make this nicer for you.

This is effectively what we've discussed. The main difference is that the properties are only exposed as public members if you pass true to an optional second parameter in the type provider, so this now works:

type MainWindow = XAML<"MainWindow.xaml", true>

type MyView(xaml : MainWindow) =
    inherit View<MyEvents, MyModel>(xaml.Root)
    ...

I ported one of your samples to FsXaml and included it in the demos here: https://github.com/fsprojects/FsXaml/blob/master/demos/FSharpDesktopUINumericUpDown/Program.fs#L65-L66

It demonstrates using the type provider with App.xaml (for global styles) and MainWindow.xaml (for the View, now defined in XAML).

I'm going to close this issue - but feel free to reopen/comment if this isn't a workable option for you.

dmitry-a-morozov commented 10 years ago

@ReedCopsey Nice effort. I will keep working on more examples for FSharp.Desktop.UI. I will demo usage of FsXaml.XAML type provider in one of those. Overall, I put major design effort to enable implementation flexibility. It's up to developer to use either TP, C# gen code, manual xaml loading or something else.