Closed houstonhaynes closed 1 year ago
FWIW I'm on MacOS M1 on the latest version of Rider. (2023.1.3)
<PackageReference Include="System.Reactive.Core" Version="5.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-preview5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-preview5" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-preview5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-preview5" />
<PackageReference Include="LiveChartsCore" Version="2.0.0-beta.801" />
<PackageReference Include="LiveChartsCore.SkiaSharpView" Version="2.0.0-beta.801" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-beta.800-11.0.0-rc1.1" />
<ProjectReference Include="..\..\Elmish.Avalonia\Elmish.Avalonia.fsproj" />
Oh - and one more addendum - the LiveCharts2 sample app (C#) runs without issue using the same System, CommunityToolkit, Avalonia and LiveChartsCore versions
It looks like the LiveCharts package you are using references a newer version of Avalonia (rc-1), so I had to updated the references to the latest:
<ItemGroup>
<PackageReference Include="System.Reactive.Core" Version="5.0.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0-rc1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0-rc1.1" />
<PackageReference Include="LiveChartsCore" Version="2.0.0-beta.801" />
<PackageReference Include="LiveChartsCore.SkiaSharpView" Version="2.0.0-beta.801" />
<PackageReference Include="LiveChartsCore.SkiaSharpView.Avalonia" Version="2.0.0-beta.800-11.0.0-rc1.1" />
<ProjectReference Include="..\..\Elmish.Avalonia\Elmish.Avalonia.fsproj" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
</ItemGroup>
Next fix was that your MainViewModel.fs
ShowChart
was pointing to the wrong page/vm, so I fixed that as well:
let update (msg: Msg) (model: Model) =
match msg with
| ShowCounter ->
{ model with ContentVM = CounterViewModel.vm }
| ShowChart ->
{ model with ContentVM = ChartViewModel.vm }
| ShowAbout ->
{ model with ContentVM = AboutViewModel.vm }
Next was that upgrading all the packages seemed to break the DataGrid
control used on the CounterView
page, but that's a different problem for a different day, so I just commented out the DataGrid
to bypass the error:
<!--<DataGrid Items="{Binding Actions}" AutoGenerateColumns="True" Height="400">
</DataGrid>-->
That should get you beyond the build error so you can continue playing around with the chart page.
Here are the rest of the steps:
<lvc:CartesianChart
Grid.Row="1"
MinHeight="300"
MinWidth="600"
Series="{Binding Series}">
</lvc:CartesianChart>
I added a Series
binding with some static data just to see it render:
let series =
let lineSeries = LineSeries<int>(Values = [1 .. 10], Fill = null, Name = "Income") :> ISeries
[|
lineSeries
|]
let bindings () : Binding<Model, Msg> list = [
"Actions" |> Binding.oneWay (fun m -> m.Actions)
"AddItem" |> Binding.cmd AddItem
"RemoveItem" |> Binding.cmd RemoveItem
"UpdateItem" |> Binding.cmd UpdateItem
"ReplaceItem" |> Binding.cmd ReplaceItem
"Series" |> Binding.oneWay (fun m -> series)
]
What you were trying to do with the MVVM bindings... you can probably mix and match MVVM + Elmish, but I would be inclined to just use Elmish rather than mix paradigms.
I made sure this framework is updated to use the latest Elmish v4, so you should be able to use its new subscriptions feature to subscribe to the observable and push the new value into model.Series
or something like that.
But if you really want to use MVVM, you will need to use a VM class with property members for the RelayCommand
s, not a module w/ let bindings.
Sidestepping the whole observable thing (because I'm not sure what you need to do), here is a very simple version that adds random numbers:
module AvaloniaExample.ViewModels.ChartViewModel
open System
open System.Collections.ObjectModel
open CommunityToolkit.Mvvm.Input
open Elmish.Avalonia
open LiveChartsCore
open LiveChartsCore.Defaults
open LiveChartsCore.SkiaSharpView
let _random = Random()
type Model =
{
Data: int list
}
type Msg =
| AddItem
let init() =
{
Data = []
}
let update (msg: Msg) (model: Model) =
match msg with
| AddItem ->
{ model with
Actions = model.Actions @ [ { Description = "AddItem" } ]
Data = model.Data @ [ _random.Next(0, 10) ]
}
let bindings () : Binding<Model, Msg> list = [
"AddItem" |> Binding.cmd AddItem
"Series" |> Binding.oneWay (fun m ->
[|
LineSeries<int>(Values = m.Data, Fill = null, Name = "Income") :> ISeries
|]
)
]
let designVM = ViewModel.designInstance (init()) (bindings())
let vm = ElmishViewModel(AvaloniaProgram.mkSimple init update bindings)
Make sure you update your Button
Command
binding to AddItem
(not AddItemCommand
) so it matches your binding:
<Button Margin="6" Command="{Binding AddItem}">Add</Button>
Thanks so much for all of this - quite an education - some of which I'm still absorbing. I have to admit that it was a little confusing to see an example with a "ViewModel" folder in what I thought would be an MVU structured project. So, for much of the time I was wondering if there was room in "Elmish" for MVVM.
So while I'm here - I've got to say that still confuses me a bit - why does the Elmish.Avalonia sample project have a "ViewModels" folder? What am I missing about this sample?
[FYI - my use of ObservableCollection was because It was in the LiveChart2 sample and I was modeling that code in F# (or that was the idea). I also use Observables so frequently with WildernessLabs Meadow it struck me as a mental hand-hold that I thought would be useful.]
I guess it could be a misnomer to call them view models, but this is sort of as hybrid model that uses MVU to create a view model.
The observable is certainly doable, but maybe not worth the hassle unless you really need it.
I should also add that you can have one big MVU loop if you want. I just prefer to have a localized MVU loop per view.
I agree 100% - the strongest reason for using the ObservableCollection was the "mechanical sympathy" with the C# example on the LiveCharts2 site - though they did mention that other methods were possible.
That opens up the can-o-worms on "just use functional" versus the constructor method - it's a cognitive burden thing - if a new user (like myself) can't determine what's just a convenience to the author versus what could otherwise be a more generalizable pattern... you get the point I'm sure - since you've been moving in and out of these frameworks for much longer. I've been reading "Stylish F#" and have been thinking about it as I go from app-level aping of other people's work to actually internalizing F# "intent" for the lack of a better term. (since "idiom" is in the eye of the beholder)
I like the idea of an MVU loop per view. I've also been thinking about breaking down the "code by folder" approach that's baked in to C#. I plan to blog about this at some point - more for notes for myself - but also for those who might be similarly unfamiliar with the variety of patterns out there - to help newbs like myself make sense of it as "conventions" emerge.
Sorry - spoke too soon - I'm still not seeing things work - same error.
How "picky" is this on MSBuild versions? I have three refs (.NET 6, 7, 8) and it (Rider on Mac) seems to want to use 8's MSBuild for some weird reason.
The good news - is that when I updated Avalonia.Themes.Fluent
to rc1 the preview in Rider was rendering... 👍
It looks like you skipped the first step (which happens to be the step that fixes your bug), which is to update the NuGet packages. The Graph package you are using targets the newer release candidate version of Avalonia. I posted all the NuGet packages above so you can just copy/paste them into your project file exactly I as I have them.
Also, you still need to rename your button command bindings to match the bindings in your VM.
Dang - thanks - I thought I had gotten all of them. Copy-paste it is!
Got it working - had to add Actions back to your one-pager. Really nice, tight layout. Fun! 🥇
module AvaloniaExample.ViewModels.ChartViewModel
open System
open Elmish.Avalonia
open LiveChartsCore
open LiveChartsCore.SkiaSharpView
let _random = Random()
type Model =
{
Data: int list
Actions: Action list
}
and Action =
{
Description: string
}
type Msg =
| AddItem
let init() =
{
Data = []
Actions = [ { Description = "Init" } ]
}
let update (msg: Msg) (model: Model) =
match msg with
| AddItem ->
{ model with
Actions = model.Actions @ [ { Description = "AddItem" } ]
Data = model.Data @ [ _random.Next(0, 10) ]
}
let bindings () : Binding<Model, Msg> list = [
"AddItem" |> Binding.cmd AddItem
"Series" |> Binding.oneWay (fun m ->
[|
LineSeries<int>(Values = m.Data, Fill = null, Name = "Income") :> ISeries
|]
)
]
let designVM = ViewModel.designInstance (init()) (bindings())
let vm = ElmishViewModel(AvaloniaProgram.mkSimple init update bindings)
It does look very clean!
And FWIW I think I found the reason why using ObservableCollection is "better" than just using a list... I think - maybe you can weigh in on this.
When you "update" in the current implementation you're really dropping in an entirely new list - which means the Series re-draws. (animates from the "bottom" X axis upward) But what I'm looking for is what LiveCharts2 refers to as AutoUpdate or as marked in the on-screen control - "Constant Updates" https://youtu.be/cwImMqLgAxg?t=2216
The way that I read it, it's much like the virtual DOM in Blazor, where LiveCharts "sees" the change and performs the update without a full redraw - and manages the transition between the "current" and "next" variation of the set.
TBH, you may want to use a classic view model class for this page with the live chart. I think the immutable model approach isn't the best fit here. However, you could maybe hack it by storing the chart data in a mutable variable like this (but I'm not 100% sure if this will accomplish what you are trying to do):
module AvaloniaExample.ViewModels.ChartViewModel
open System
open Elmish.Avalonia
open LiveChartsCore
open LiveChartsCore.SkiaSharpView
open System.Collections.ObjectModel
open LiveChartsCore.Defaults
let _random = Random()
type Model =
{
Data: int list
Actions: Action list
}
and Action =
{
Description: string
}
type Msg =
| AddItem
| RemoveItem
| UpdateItem
| ReplaceItem
let mutable series : ObservableCollection<ISeries> = null
let mutable observableValues : ObservableCollection<ObservablePoint> = null
let init() =
observableValues <- ObservableCollection<ObservablePoint>()
series <-
ObservableCollection<ISeries>
[
ColumnSeries<ObservablePoint>(Values = observableValues) :> ISeries
]
{
Data = []
Actions = [ { Description = "AddItem"} ]
}
let update (msg: Msg) (model: Model) =
match msg with
| AddItem ->
observableValues.Add(ObservablePoint(_random.Next(0, 10), _random.Next(0, 10)))
{ model with
Actions = model.Actions @ [ { Description = "AddItem" } ]
Data = model.Data @ [ _random.Next(0, 10) ]
}
| RemoveItem ->
{ model with
Actions = model.Actions @ [ { Description = "RemoveItem" } ]
Data = model.Data |> List.rev |> List.tail |> List.rev
}
| UpdateItem ->
{ model with
Actions = model.Actions @ [ { Description = "UpdateItem" } ]
Data = model.Data |> List.rev |> List.tail |> List.rev |> List.map (fun x -> x + 1)
}
| ReplaceItem ->
{ model with
Actions = model.Actions @ [ { Description = "ReplaceItem" } ]
Data = model.Data |> List.rev |> List.tail |> List.rev |> List.map (fun x -> _random.Next(0, 10))
}
let bindings () : Binding<Model, Msg> list = [
"Actions" |> Binding.oneWay (fun m -> m.Actions)
"AddItem" |> Binding.cmd AddItem
"RemoveItem" |> Binding.cmd RemoveItem
"UpdateItem" |> Binding.cmd UpdateItem
"ReplaceItem" |> Binding.cmd ReplaceItem
"Series" |> Binding.oneWay (fun m -> series)
]
let designVM = ViewModel.designInstance (init()) (bindings())
let vm = ElmishViewModel(AvaloniaProgram.mkSimple init update bindings)
Thanks - I had something similar. It's interesting that Adrii Chebukin said that a mutable ObservableCollection is not necessary. I'm accustomed to making an explicit declaration it seems weird (and obfuscating) not to do so as a reflex.
You could try using this binding to guarantee that the "Series" binding won't ever be replaced (since it is mutable):
"Series" |> Binding.oneWayLazy ((fun m -> series), (fun a b -> true), id)
The second function fun a b -> true
is an equality test to see if "Series" has changed, and we always return true
, so it should keep reusing the same "Series" value.
But it looks the same to me either way. The graph doesn't appear to be resetting regardless of which binding is used. 🤷♂️
This is starting to be REALLY fun... I thought the way to skirt the render of the bar chart/histogram was to change to ObservableCollection<ObservableValue>
[instead of ObservablePoint]
I'm looking forward to adding a timestamp to the Y axis, which is my next little bit of "flair" so getting that early look into using the ObservablePoint was a nice brain teaser. 😸
I see what you mean about the oneWayLazy
binding - sort of - those first two functions are unused but the signature fails without the "unused" entries. That's something for me to understand more deeply.
It is definitely using the first two functions. The linting message is just pointing out that we are ignoring the m
parameter (which we are since we're doing something hacky).
But you can fix that by discarding the params:
"Series" |> Binding.oneWayLazy ((fun _ -> series), (fun _ _ -> true), id)
It also occurred to me that we can simply store it on the Model
. No need to put it in the ugly mutable value. It doesn't matter that the model is recreated each time as we are taking care to not replace the actual chart data, and we are using the oneWayLazy
binding to ensure that the same object is used.
module AvaloniaExample.ViewModels.ChartViewModel
open System
open Elmish.Avalonia
open LiveChartsCore
open LiveChartsCore.SkiaSharpView
open System.Collections.ObjectModel
open LiveChartsCore.Defaults
let _random = Random()
type Model =
{
Series: ObservableCollection<ISeries>
Actions: Action list
}
and Action =
{
Description: string
}
type Msg =
| AddItem
| RemoveItem
| UpdateItem
| ReplaceItem
let init() =
{
Series =
ObservableCollection<ISeries>
[
LineSeries<ObservablePoint>(Values = ObservableCollection<ObservablePoint>()) :> ISeries
]
Actions = [ { Description = "AddItem"} ]
}
let update (msg: Msg) (model: Model) =
match msg with
| AddItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservablePoint>
let len = values.Count
values.Add(ObservablePoint(values.Count, _random.Next(1, 10)))
{ model with Actions = model.Actions @ [ { Description = "AddItem" } ] }
| RemoveItem ->
{ model with Actions = model.Actions @ [ { Description = "RemoveItem" } ] }
| UpdateItem ->
{ model with Actions = model.Actions @ [ { Description = "UpdateItem" } ] }
| ReplaceItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservablePoint>
let fstValue = values[0]
values[0] <- ObservablePoint(fstValue.X, 10)
{ model with Actions = model.Actions @ [ { Description = "ReplaceItem" } ] }
let bindings () : Binding<Model, Msg> list = [
"Actions" |> Binding.oneWay (fun m -> m.Actions)
"AddItem" |> Binding.cmd AddItem
"RemoveItem" |> Binding.cmd RemoveItem
"UpdateItem" |> Binding.cmd UpdateItem
"ReplaceItem" |> Binding.cmd ReplaceItem
"Series" |> Binding.oneWayLazy ((fun m -> m.Series), (fun _ _ -> false), id)
]
let designVM = ViewModel.designInstance (init()) (bindings())
let vm = ElmishViewModel(AvaloniaProgram.mkSimple init update bindings)
Thanks - I was actually wondering about that as I was "outside" the Model and wondering if that was "canonical" for the lack of a better term.
Alright - I'm about to let you off the hook - but there's just ONE more thing... 😸
I have set a recursive loop in the update - which might be a big no-no... since things have kind of gone sideways. But I thought I'd show you what I've done to try to ape the C# version of the "isStreaming" method.
Of course first I'd like to know just how wrong this is, and secondly I think this broke my build - as now I'm getting a bunch of errors that don't make any sense at all (like LiveChartsCore is not defined even though it's clearly part of the project)
let update (msg: Msg) (model: Model) =
// new section
let mutable isContinuing = false
let runContinuous () =
async {
let rec streamingLoop () =
if isContinuing then
let values = model.Series.[0].Values :?> ObservableCollection<ObservableValue>
values.RemoveAt(0)
values.Add(ObservableValue(_random.Next(1, 11)))
Task.Delay(1000) |> Async.AwaitTask |> ignore
streamingLoop ()
streamingLoop ()
}
runContinuous () |> Async.StartImmediate
// end new section
match msg with
| AddItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservableValue>
values.Add(ObservableValue(_random.Next(1, 11)))
{ model with
Actions = model.Actions @ [ { Description = "AddItem" } ]
}
| RemoveItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservableValue>
values.RemoveAt(0)
{ model with
Actions = model.Actions @ [ { Description = "RemoveItem" } ]
}
| UpdateItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservableValue>
let len = values.Count
let item = _random.Next(0, len)
values[item] <- ObservableValue(_random.Next(1, 11))
{ model with
Actions = model.Actions @ [ { Description = "UpdateItem" } ]
}
| ReplaceItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservableValue>
let lstValue = values.Count-1
values[lstValue] <- ObservableValue(_random.Next(1, 11))
{ model with
Actions = model.Actions @ [ { Description = "ReplaceItem" } ]
}
| Continue ->
match isContinuing with
| false ->
isContinuing <- true
{ model with
Actions = model.Actions @ [ { Description = "Continue" } ]
}
| _ ->
isContinuing <- false
{ model with
Actions = model.Actions @ [ { Description = "Continue" } ]
}
and here's what I'm trying to bring over from C#
Thanks!
The problem is that you have introduced an effect into update
function which should be pure.
You should instead pull the side effect out of the update
fn via the Elmish subscription system.
You will like this:
module AvaloniaExample.ViewModels.ChartViewModel
open System
open Elmish.Avalonia
open LiveChartsCore
open LiveChartsCore.SkiaSharpView
open System.Collections.ObjectModel
open LiveChartsCore.Defaults
let _random = Random()
type Model =
{
Series: ObservableCollection<ISeries>
Actions: Action list
}
and Action =
{
Description: string
}
type Msg =
| AddItem
| RemoveItem
| UpdateItem
| ReplaceItem
| Reset
let init() =
{
Series =
ObservableCollection<ISeries>
[
LineSeries<ObservablePoint>(Values = ObservableCollection<ObservablePoint>()) :> ISeries
]
Actions = [ { Description = "AddItem"} ]
}
let update (msg: Msg) (model: Model) =
match msg with
| AddItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservablePoint>
values.Add(ObservablePoint(values.Count, _random.Next(1, 10)))
{ model with Actions = model.Actions @ [ { Description = "AddItem" } ] }
| RemoveItem ->
{ model with Actions = model.Actions @ [ { Description = "RemoveItem" } ] }
| UpdateItem ->
{ model with Actions = model.Actions @ [ { Description = "UpdateItem" } ] }
| ReplaceItem ->
let values = model.Series[0].Values :?> ObservableCollection<ObservablePoint>
let fstValue = values[0]
values[0] <- ObservablePoint(fstValue.X, 10)
{ model with Actions = model.Actions @ [ { Description = "ReplaceItem" } ] }
| Reset ->
let values = model.Series[0].Values :?> ObservableCollection<ObservablePoint>
{ model with Actions = model.Actions @ [ { Description = "Reset" } ] }
let bindings () : Binding<Model, Msg> list = [
"Actions" |> Binding.oneWay (fun m -> m.Actions)
"AddItem" |> Binding.cmd AddItem
"RemoveItem" |> Binding.cmd RemoveItem
"UpdateItem" |> Binding.cmd UpdateItem
"ReplaceItem" |> Binding.cmd ReplaceItem
"Series" |> Binding.oneWayLazy ((fun m -> m.Series), (fun _ _ -> false), id)
]
let designVM = ViewModel.designInstance (init()) (bindings())
open Elmish
open System.Timers
let subscriptions (model: Model) : Sub<Msg> =
let valueChangedSubscription (dispatch: Msg -> unit) =
let timer = new Timer(500) // Fire twice per second
timer.Elapsed.Add(fun _ ->
dispatch AddItem
)
timer.Start()
timer :> IDisposable
[
[ nameof valueChangedSubscription ], valueChangedSubscription
]
let vm = ElmishViewModel(
AvaloniaProgram.mkSimple init update bindings
|> AvaloniaProgram.withSubscription subscriptions
)
LOVELY! I do likely... It kinda reminds me of the subscription model in WildernessLabs
I particularly like the dispatch. Very tidy!
Thanks so much!
You'll be proud of me 😆
I switched over from ObservableCollection<ObservableValue>
to <DateTimePoint>
and managed to bind the XAxes definition without having to come up for air to ask you! 🦜 😁
The "odd" thing I ran into was that the add/remove/updates where "backward" and creating an odd behavior.
What I figured out after a hot minute ⏲️ was that I had initialized the series in ascending order (following the index value I used to create the DateTimeOffset) and the chart was dutifully re-ordering according to the XAxis. That meant when I RemoveAt[0]
it was getting right of the last in the series visually (because of the DateTime order). I laughed pretty hard at myself when I finally figured out to switch from
for i from 0 .. 10 do
to
for i = 10 downto 0 do
then everything "just worked" 👍 🙄
Thanks again - I realize this was a pretty hefty investment on your part, but I hope to pay it forward by both writing about this in my yet-to-be-published blog - FSharpNotes and perhaps add a video or two to go with it as the project continues.
Super cool! It looks really good with the labels.
Small thing, but this line:
let newSeries =
should probably be changed to a function so that it is always reinitialized when it is called from the init
function:
let newSeries () =
THERE IT IS! I knew I had missed something.
BTW - I want to grab the toggleButton and set IsChecked <- false
but "cheat" my way through with a simple
let toggleButton = parentControl.FindControl("AutoUpdate") :?> ToggleButton
I'm getting "lit up" but not really certain what the "parent control" actually means in the User Control context. Is it looking for an overall User Control "Name" reference? Why can't we just use a "this" value? (like - I'm looking here why don't you infer that?)
I've been lurking here and enjoying the journey. If you're both up for it, this would be a great addition to the samples when its complete.
Thanks! I was wondering about building something that folks just "just build and run" without having to dig through the original project structure. Now that Elmish.Avalonia is available on nuget I think that's a big step forward.
I had also thought about fleshing out LiveChart2's sample app in this model - just as an exercise to create an F# equivalent. They have separated out some partial classes, which makes sense for their re-use in various examples from the same base visual - but I'm not sure that really suits the "garden path" metaphor I think would be useful to new folks here.
And I like the idea of an Elmish.Avalonia template as a first bookend for the garden path experience. Since this still an alpha I'm sure @JordanMarr already has his own ideas. 🧠 😁
On Wed, Jun 28, 2023 at 04:45:50AM -0700, Houston Haynes wrote:
Thanks! I was wondering about building something that folks just "just build and run" without having to dig through the original project structure. Now that Elmish.Avalonia is available on nuget I think that's a big step forward.
Not a strong push here, but my vote would still be keep it with the project code. I run into so many beautiful beautiful open source projects with minimal docs (which I totally understand, it's hard work to write / maintain docs), and often the docs aren't super helpful. A fully worked example is golden. Most productive coding is copying and hacking a working example. Once you're super familar with a library, then the template and an empty page is good, but the number of times I would have killed for just any example of X in a project (I filed a bug report recently on a feature in another library that I just couldn't get working after days and concluded was actually buggy - I couldn't find a single example anywhere on the internet or code base of it being used. Finally found another bug report, and then realized it had never worked. A single positive example with the code would have been an existence proof. Slightly more work for maintainer, but you can also build the demo projects with CI and they offer a smoke test that the library still works with "real code". I'll stop here, but I love the samples folder in a project..
THERE IT IS! I knew I had missed something.
BTW - I want to grab the toggleButton and set
IsChecked <- false
but "cheat" my way through with a simplelet toggleButton = parentControl.FindControl("AutoUpdate") :?> ToggleButton
I'm getting "lit up" but not really certain what the "parent control" actually means in the User Control context. Is it looking for an overall User Control "Name" reference? Why can't we just use a "this" value? (like - I'm looking here why don't you infer that?)
You definitely want to avoid hacking the xaml control hierarchy in your MVU code. (If you have to resort to that, it would be better to do it in the code-behind file.)
If you are trying to enable/disable buttons that are bound to a command (Add, Remove, etc), you can use Binding.cmdIf
:
"AddItem" |> Binding.cmdIf (fun m -> if m.Actions.Length > 5 then Some AddItem else None)
Alternatively, you could create a CanAddItem
binding and manually bind it to the button:
<Button Margin="6" IsEnabled="{Binding CanAddItem}" Command="{Binding AddItem}">Add</Button>
"AddItem" |> Binding.cmd AddItem
"CanAddItem" |> Binding.oneWay (fun m -> m.Actions.Length > 5)
I have forked this repo and am trying to add a chart based on the (C#) sample on the LiveCharts2 site.
I've done my best to muddle through (with some support on the F# Discord) and can get things to "compile" - BUT the reason for the quotes is that when I run Debug I get an unhandled exception:
This is my first try at this so I'm casting around for any help I can find. Thanks.