I'm trying to put together an app, but I've run into problems trying to get data into an IDrawable graph display. The app is just supposed to show a plot of random data from a float[] which is then updated when the refresh button is clicked.
The approach that seemed most Mvvm-ish was to make the IDrawable a BindableObject (mainly from How to pass variable data into .NET MAUI GraphicsView canvas and In .NET MAUI, how do I pass variables to a GraphicsView.Drawable in a ContentView).
The problem is that the view is never updated: Draw is called once when the app starts (but the data is null), and then never again. The data held in the ViewModel is being updated correctly, but those changes don't appear to be getting passed on. It's my understanding that when a change in the data is detected, the view will automatically be Invalidate()-d and the graph should be redrawn.`
(I'm using VS2022 Community 17.14.12, .NET 8.0 with the CommunityToolkit.Mvvm 8.3.2.)
Here's the code for the `IDrawable':
namespace TestNET8.Drawables;
public partial class GraphDrawable : BindableObject, IDrawable
{
public float[] Data
{
get => (float[])GetValue(DataProperty);
set => SetValue(DataProperty, value);
}
public static readonly BindableProperty DataProperty =
BindableProperty.Create(nameof(Data), typeof(float[]), typeof(GraphDrawable));
public void Draw(ICanvas canvas, RectF dirtyRect)
{
if (Data != null && Data.Length > 0)
{
for (int i = 0; i < Data.Length - 1; i++)
{
canvas.DrawLine(5 * i, 100 * Data[i], 5 * (i + 1), 100 * Data[i + 1]);
}
}
}
}
The view model:
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace TestNET8.ViewModels;
public partial class MainViewModel : ObservableObject
{
private float[] dataHolder = new float[100];
public float[] DataHolder
{
get => dataHolder;
set
{
if (dataHolder == value) return;
dataHolder = value;
OnPropertyChanged();
}
}
[RelayCommand]
public void Refresh()
{
var rand = new Random();
float[] temp = new float[100];
for (int i = 0; i < 100; i++)
{
temp[i] = rand.NextSingle();
}
DataHolder = temp;
}
}
and the xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:viewmodel="clr-namespace:TestNET8.ViewModels"
xmlns:drawables="clr-namespace:TestNET8.Drawables"
x:DataType="viewmodel:MainViewModel"
x:Class="TestNET8.MainPage">
<VerticalStackLayout>
<GraphicsView
HeightRequest="200"
WidthRequest="500">
<GraphicsView.Drawable>
<drawables:GraphDrawable Data="{Binding DataHolder}"/>
</GraphicsView.Drawable>
</GraphicsView>
<Button
Text="Refresh"
Command="{Binding RefreshCommand}"/>
</VerticalStackLayout>
</ContentPage>
The MainPage code-behind is just:
public partial class MainPage : ContentPage
{
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = vm;
}
}
and MauiProgram.cs contains the lines
builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainViewModel>();
builder.Services.AddSingleton<GraphDrawable>();
Edit: As everyone pointed out, I still need to Invalidate() the view when the property changes. So I added a reference to the view in the xaml:
x:Name="GraphView"
and updated the code-behind to respond to the property change event:
using TestNET8.ViewModels;
namespace TestNET8
{
public partial class MainPage : ContentPage
{
readonly MainViewModel viewModel;
public MainPage(MainViewModel vm)
{
InitializeComponent();
BindingContext = vm;
viewModel = vm;
}
protected override void OnAppearing()
{
base.OnAppearing();
viewModel.PropertyChanged += ViewModel_PropertyChanged;
}
protected override void OnDisappearing()
{
viewModel.PropertyChanged -= ViewModel_PropertyChanged;
base.OnDisappearing();
}
private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
GraphView.Invalidate();
}
}
}
This seemed like a Mvvm way of doing it, but please let me know if there's a better way.
The button click now redraws the GraphicsView and refreshes the DataHolder array in the view model, but when Draw is called, the Data array in the GraphDrawable is null. So there seems to be something wrong with my binding.
