adding LiveCharts2 to a view (RenderLoop error?) #9
Replies: 42 comments 20 replies
-
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" /> |
Beta Was this translation helpful? Give feedback.
-
Oh - and one more addendum - the LiveCharts2 sample app (C#) runs without issue using the same System, CommunityToolkit, Avalonia and LiveChartsCore versions |
Beta Was this translation helpful? Give feedback.
-
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 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 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. |
Beta Was this translation helpful? Give feedback.
-
Here are the rest of the steps:
<lvc:CartesianChart
Grid.Row="1"
MinHeight="300"
MinWidth="600"
Series="{Binding Series}">
</lvc:CartesianChart> I added a 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)
] |
Beta Was this translation helpful? Give feedback.
-
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 But if you really want to use MVVM, you will need to use a VM class with property members for the |
Beta Was this translation helpful? Give feedback.
-
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: ChartViewModel.fsmodule 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 Margin="6" Command="{Binding AddItem}">Add</Button> |
Beta Was this translation helpful? Give feedback.
-
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.] |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
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. Also, you still need to rename your button command bindings to match the bindings in your VM. |
Beta Was this translation helpful? Give feedback.
-
Dang - thanks - I thought I had gotten all of them. Copy-paste it is! |
Beta Was this translation helpful? Give feedback.
-
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) |
Beta Was this translation helpful? Give feedback.
-
It does look very clean! |
Beta Was this translation helpful? Give feedback.
-
You'll be proud of me 😆 I switched over from 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 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. |
Beta Was this translation helpful? Give feedback.
-
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 let newSeries () = |
Beta Was this translation helpful? Give feedback.
-
THERE IT IS! I knew I had missed something. BTW - I want to grab the toggleButton and set 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?) |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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. 🧠 😁 |
Beta Was this translation helpful? Give feedback.
-
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..
|
Beta Was this translation helpful? Give feedback.
-
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 "AddItem" |> Binding.cmdIf (fun m -> if m.Actions.Length > 5 then Some AddItem else None) Alternatively, you could create a <Button Margin="6" IsEnabled="{Binding CanAddItem}" Command="{Binding AddItem}">Add</Button> "AddItem" |> Binding.cmd AddItem
"CanAddItem" |> Binding.oneWay (fun m -> m.Actions.Length > 5) |
Beta Was this translation helpful? Give feedback.
-
It sounds like "IsAutoUpdateChecked" should be a two-way binding. |
Beta Was this translation helpful? Give feedback.
-
So first of all, drop the notion of using the <ToggleButton Name="AutoUpdate" Margin="6" IsChecked="{Binding IsAutoUpdateChecked}">AutoUpdate</ToggleButton> On the MVU side, we can remove all the procedural WPF code and instead we want to focus on our abstraction. Full code is listed at the bottom, but here is a breakdown:
Here's the full code: module AvaloniaExample.ViewModels.ChartViewModel
open System
open System.Collections.Generic
open System.Collections.ObjectModel
open Elmish.Avalonia
open Avalonia.Controls.Primitives
open LiveChartsCore
open LiveChartsCore.Kernel.Sketches
open LiveChartsCore.SkiaSharpView
open LiveChartsCore.Defaults
let _random = Random()
let newSeries (count: int option) =
let newCollection = ObservableCollection<DateTimePoint>()
// use seriesCount to either 1) set a default 15 at init or 2) use the count passed in from the reset button
let mutable seriesCount = 0
match count with
| None ->
seriesCount <- 15
| _ ->
seriesCount <- count.Value - 1
for i = seriesCount downto 0 do
// backdate the time in seconds by the index to create a series of points in the past
let past = DateTimeOffset.Now.AddSeconds(-i).LocalDateTime
let _randomNull = _random.Next(0, 99)
// in 1% of cases produce a null value to show an "empty" spot in the series
match _randomNull with
| i when i = 0 ->
newCollection.Add(DateTimePoint(past, System.Nullable()))
| _ -> newCollection.Add(DateTimePoint(past, _random.Next(0, 10)))
newCollection
let XAxes : IEnumerable<ICartesianAxis> =
[| Axis (
Labeler = (fun value -> DateTime(int64 value).ToString("HH:mm:ss")),
LabelsRotation = 15,
UnitWidth = float(TimeSpan.FromSeconds(1).Ticks),
MinStep = float(TimeSpan.FromSeconds(1).Ticks)
)
|]
type Model =
{
Series: ObservableCollection<ISeries>
Actions: Action list
IsAutoUpdateChecked: bool
}
and Action =
{
Description: string
}
type Msg =
| AddItem
| AddNull
| RemoveItem
| UpdateItem
| ReplaceItem
| Reset
| SetIsAutoUpdateChecked of bool
let init () =
{
Series =
ObservableCollection<ISeries>
[
LineSeries<DateTimePoint>(Values = newSeries(None), Fill = null, Name = "Luck By Second") :> ISeries
]
Actions = [ { Description = "Initialized"} ]
IsAutoUpdateChecked = false
}
let update (msg: Msg) (model: Model) =
let values = model.Series[0].Values :?> ObservableCollection<DateTimePoint>
match msg with
| AddItem ->
values.Insert(values.Count, (DateTimePoint(DateTime.Now, _random.Next(0, 10))))
{ model with
Actions = model.Actions @ [ { Description = "AddItem" } ]
}
| AddNull ->
values.Insert(values.Count, (DateTimePoint(DateTime.Now, System.Nullable())))
{ model with
Actions = model.Actions @ [ { Description = "AddNull" } ]
}
| RemoveItem ->
values.RemoveAt(0)
{ model with
Actions = model.Actions @ [ { Description = "RemoveItem" } ]
}
| UpdateItem ->
let item = _random.Next(0, values.Count - 1)
let fstValueTime = values.[item].DateTime
values[item] <- DateTimePoint(fstValueTime, _random.Next(0, 10))
{ model with
Actions = model.Actions @ [ { Description = "UpdateItem" } ]
}
| ReplaceItem ->
let lastValueTime = values[values.Count - 1].DateTime
values[values.Count - 1] <- DateTimePoint(lastValueTime, _random.Next(0, 10))
{ model with
Actions = model.Actions @ [ { Description = "ReplaceItem" } ]
}
| Reset ->
// pass up the current length of the series to the newSeries function
model.Series[0].Values <- newSeries(Some values.Count)
{ model with
IsAutoUpdateChecked = false
Actions = model.Actions @ [ { Description = "Reset" } ]
}
| SetIsAutoUpdateChecked isChecked ->
{ model with
IsAutoUpdateChecked = isChecked
Actions = model.Actions @ [ { Description = $"IsAutoUpdateChecked: {isChecked}" } ]
}
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
"Reset" |> Binding.cmd Reset
"IsAutoUpdateChecked" |> Binding.twoWay ((fun m -> m.IsAutoUpdateChecked), SetIsAutoUpdateChecked)
"Series" |> Binding.oneWayLazy ((fun m -> m.Series), (fun _ _ -> true), id)
"XAxes" |> Binding.oneWayLazy ((fun _ -> XAxes), (fun _ _ -> true), id)
]
let designVM = ViewModel.designInstance (init()) (bindings())
open Elmish
open System.Timers
let subscriptions (model: Model) : Sub<Msg> =
let autoUpdateSubscription (dispatch: Msg -> unit) =
let timer = new Timer(1000)
let disposable =
timer.Elapsed.Subscribe(fun _ ->
let _randomNull = _random.Next(0, 99)
match _randomNull with
| i when i = 0 ->
dispatch AddNull
| _ -> dispatch AddItem
dispatch RemoveItem
)
timer.Start()
disposable
[
if model.IsAutoUpdateChecked then
[ nameof autoUpdateSubscription ], autoUpdateSubscription
]
let vm = ElmishViewModel(
AvaloniaProgram.mkSimple init update bindings
|> AvaloniaProgram.withSubscription subscriptions
) |
Beta Was this translation helpful? Give feedback.
-
On Wed, Jun 28, 2023 at 04:14:51PM -0700, Jordan Marr wrote:
So first of all, drop the notion of using the `ToggleButton` `Command`. Instead, we will drive everything using a two-way binding to `IsChecked`:
```xaml
<ToggleButton Name="AutoUpdate" Margin="6" IsChecked="{Binding IsAutoUpdateChecked}">AutoUpdate</ToggleButton>
```
This is one of my favorite features in the the library Jordan - being able to manage all of that fiddly UI state simply by connecting a property to a model variable is just the way it should be
|
Beta Was this translation helpful? Give feedback.
-
The syntax on the two way binding is not very intuitive. I had to look up an example on the Elmish.WPF repo. Also, the new Elmish v4 subscription system is extremely unintuitive. I have to look at examples every time to get it right, even though I did the upgrade from v3 to v4 on the Elmish.WPF repo. |
Beta Was this translation helpful? Give feedback.
-
It looks like the |
Beta Was this translation helpful? Give feedback.
-
FWIW - I tried to add the "Ok" button to the bottom of the ChartView so that it would also return to the CounterView like About (showing the global being used in more than one place) and it REALLY started lighting up the model in a way that made it a mess for me to deal with. I'll eventually jump back in and unpack that as a "learning opportunity" but for now I have to turn my attention back to WildernessLabs Meadow. |
Beta Was this translation helpful? Give feedback.
-
For the chart page, the open Messaging | Ok ->
bus.OnNext(GlobalMsg.GoHome)
{ model with IsAutoUpdateChecked = false } That's it! |
Beta Was this translation helpful? Give feedback.
-
I don't know how I missed this before (because I definitely looked for it), but I found a better way to call the message bus using AboutViewModel.fsmodule AvaloniaExample.ViewModels.AboutViewModel
open Elmish.Avalonia
open Elmish
open Messaging
type Model =
{
Version: string
}
type Msg =
| Ok
let init() =
{
Version = "1.1"
}, Cmd.none
let update (msg: Msg) (model: Model) =
match msg with
| Ok ->
model, Cmd.ofEffect (fun _ -> bus.OnNext(GlobalMsg.GoHome))
let bindings () : Binding<Model, Msg> list = [
"Version" |> Binding.oneWay (fun m -> m.Version)
"Ok" |> Binding.cmd Ok
]
let designVM = ViewModel.designInstance (fst (init())) (bindings())
let vm = ElmishViewModel(AvaloniaProgram.mkProgram init update bindings)
|
Beta Was this translation helpful? Give feedback.
-
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.
Beta Was this translation helpful? Give feedback.
All reactions