fsprojects / FsXaml

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

FsXaml for UserControls #16

Closed cloudRoutine closed 10 years ago

cloudRoutine commented 10 years ago

I've been attempting to find a way to use FsXaml to access a UserControl that's defined in its own individual XAML file, but I've been unable to find a method that works.

For example when "ListBoxQuery.xaml" defines a UserControl

 type ListBoxQueryXaml    = XAML<"ListBoxQuery.xaml">
 let queryUI              = new ListBoxQueryXaml()
 let usercontrol          = ListBoxQueryXaml.Accessor(xamlui)

is the only way I've been able to access the elements defined in the XAML file, but this method fails because at runtime attempting to use the elements throws a null object exception.

When these types like ListBoxQueryXaml are compiled to a class library and that .dll is loaded into the XAML Designer by adding it to the toolbox it does create XAML elements that can be embedded into other windows and panels. Although this isn't particularly useful as I haven't been able to give them any functionality using F# code beyond the default behavior of their standard control subcomponents.

I've had great success using FsXaml for individual WPF GUI windows, but perhaps I have a fundamental misunderstanding of how it should be applied in the creation of custom controls?

ReedCopsey commented 10 years ago

In general, the thought was that the "normal" use case would be via an MVVM approach, in which case you very rarely (almost never) would use code behind, preferring usage via binding. As such, this is rarely needed.

That being said, your code, (almost) as written, should actually work. You do have a typo in the code, however - which may be causing the issue:

 type ListBoxQueryXaml    = XAML<"ListBoxQuery.xaml">

 let queryUI              = new ListBoxQueryXaml()
 let usercontrol          = ListBoxQueryXaml.Accessor(queryUI)  // should be queryUI, not xamlui here!

Other possible issues would potentially be when you call this in your project (the thread affinity of WPF in general causes can strange things to happen if this is written prior to the entry point).

That being said, the intended mechanism for getting "code behind" style access is actually through a custom ViewController. Since we can't create a partial class ala-C#, we create a separate type that provides the implementation for "code behind" and reference that. The main difference in terms of funcitonality is that the IViewController.Attach method runs when the control gets loaded, not on construction.

I had a demo showing this off for a Window, but just added it to one of the custom UserControls in the demos. You can see this approach in action.

The first part is to add the definition to your code behind file. Bascially, when you defined your view, make a type that implements IViewController. The demo just demonstrates subscribing to MouseDoubleClick on a text box. You then add an attached property to the XAML for the UserControl to say to use this custom IViewController.

For details, see controller implementation here: https://github.com/fsprojects/FsXaml/blob/master/demos/WpfSimpleMvvmApplication/MainView.xaml.fs#L16 And XAML usage here: https://github.com/fsprojects/FsXaml/blob/master/demos/WpfSimpleMvvmApplication/MainView.xaml#L8

ReedCopsey commented 10 years ago

I take back the first portion -

It turns out that there was an issue with the way I was resolving the names in the Accessor. It would work perfectly, but only once the templates were applied (which typically happens once the user control is used, but not after construction).

I just published a new version to NuGet that provides a fallback via the Logical Tree if an element isn't found. This should correct your original issue.

That being said, I'd still recommend looking at the ViewController examples - that will be a more idiomatic way to handle code behind in FsXaml once we publish official templates.

Please try to update to the 0.9.8.1 NuGet package, and see if it corrects your issue.

cloudRoutine commented 10 years ago

Whoops, I changed the names in the code for my comment and missed one.

I updated the version of FsXaml but the accessor is still throwing errors at runtime.

Attempting to use the root of the Usercontrol throws the error

"An unhandled exception of type 'System.InvalidCastException' occurred in App.exe

Additional information: Unable to cast object of type 'CustomQueryView' to type 'System.Windows.Controls.UserControl'.

which I thought was odd as Intellisense is saying "root" has the type Controls.UserControl

ScratchModel.fs CustomQuery.xaml ScratchWindow.xaml

I looked through the ViewController examples and I'd like to start converting my projects to this model. I tried to extrapolate your WpfSimpleMvvmApplication example into a control library, but when I try to load the control into a window it throws

Error 1 Unable to load XAML data. Verify that all .xaml files are compiled as "Resource" J:\J hester\Programming Projects\WPF-Controls-FSharp\WpfSimpleMvvmApplication\MainWindow.xaml 15 9 WpfSimpleMvvmApplication

All of the xaml files are set to build as resources so I'm flummoxed about the source of this error.

BTW I'm loading the controls by referencing the ControlLibraryFs.dll in the Toolbox so I can use the control in the Designer

I uploaded the repo if you want to take a look at it -> WPF-Controls-FSharp

ReedCopsey commented 10 years ago

So - there are a few issues with the repo.

The main thing - it's actually not an FsXaml issue, but a build config issue. Your Control library was targetting 4.3.1.0 : https://github.com/cloudRoutine/WPF-Controls-FSharp/blob/master/ControlLibraryFs/ControlLibraryFs.fsproj#L13

But the main exe was 4.3.3.0: https://github.com/cloudRoutine/WPF-Controls-FSharp/blob/master/WpfSimpleMvvmApplication/WpfSimpleMvvmApplication.fsproj#L13

This causes the ViewModel to go crazy at runtime when it tries to load, which in turn makes the XAML fail to load, etc.

That being said, the VM itself has problems. First off, you have 2 CustomQueryViewModels defined in the same namespace. They also aren't defined correctly - you can't use member val with NotifyingFields in FSharp.ViewModule (it'll end up working against the initial value, but not update).

I've put in a PR for your repository that shows this working. Note that I upgraded this to the latest FsXaml, which has a nicer way of handling ViewControllers. This makes your projects work perfectly, so I'm going to close this issue.

cloudRoutine commented 10 years ago

Thanks for the help. I'm working on an MVVM desktop app for image tagging entirely in F# both to learn the ins and outs out the process and to create some modular components that can be reused on future projects, so I'm excited to see the latter is possible.

I'm in the process of converting the project to the MVVM style modeled in your demos and yesterday I realized that I couldn't use 'member val' when my commands weren't updating the viewmodel's properties.

Once the codebase is less of a mess and more of the key features are working, I plan on releasing it open source with some detailed write-ups about how to implement FsXaml and FSharp.Controls.Reactive in a desktop application. I really love using this library and I hope putting out some more concrete and in-depth examples will convince people of the viability of building desktop applications entirely in F#.

ReedCopsey commented 10 years ago

@cloudRoutine Just FYI - In general, member val will never work with INotifyPropertyChanged. The problem is that x.Value is a property, so if you write member val Foo = x.Value you end up making a member based on the current value of that property, but it doesn't actually set the property itself. I tried to get the syntax as short as possible, but you pretty much have to get and set from property to make it work.