Solved it myself by including [NofiyPropertyChangedFor] attribute to Azimuth property.
Maui view does not update Binding value correctly
Hi everyone,
I am using VS2022 Version 17.7.0 to create a C# Maui app and class myself as a beginner. I have been following Youtube videos and have my MVVM design working (to a point so far). I have the CommunityToolkit.Maui and Community Toolkit.MVVM packages installed for the project.
The project calculates the azimuth to the sun based on survey observations - angles and time and has some complex algorithms which all work fine.
I have an observation model
using CommunityToolkit.Mvvm.ComponentModel;
namespace SunObs.Maui.Models
{
public partial class Observation : ObservableObject
{
[ObservableProperty]
private int obsId;
[ObservableProperty]
private string projDescription;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(UTObs))]
private TimeSpan wT;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(UTObs))]
private DateTime obsDate;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(UTObs))]
private TimeZoneInfo timeZoneId;
[ObservableProperty]
private string latitude;
[ObservableProperty]
private string longitude;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AzRO))]
private string hCirRO;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(AzRO))]
private string hCirSun;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CalcGyro))]
private string alignmentAngle;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CalcGyro))]
private string obsGyro;
[ObservableProperty]
private double rA;
[ObservableProperty]
private double declination;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CalcGyro))]
[NotifyPropertyChangedFor(nameof(AzRO))]
private double azimuth;
[ObservableProperty]
double residual;
public DateTime UTObs => ObsDate.Add(WT - TimeZoneId.BaseUtcOffset);
public double UT => UTObs.Hour + ((double)UTObs.Minute / 60) + ((UTObs.Second + (double)UTObs.Millisecond) / 3600);
public double CalcGyro => AzRO + (double)Helpers.Deg_to_Rad(AlignmentAngle);
public double CMinusO => CalcGyro - (double)Helpers.Deg_to_Rad(ObsGyro);
public double AzRO => (double)(Azimuth - ((double)Helpers.Deg_to_Rad(HCirSun) - double)Helpers.Deg_to_Rad(HCirRO)));
public Observation()
{
}
}
A BaseViewModel;
using CommunityToolkit.Mvvm.ComponentModel;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace SunObs.Maui.ViewModel
{
public partial class BaseViewModel : ObservableObject
{
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsNotBusy))]
bool isBusy;
[ObservableProperty]
string title;
public bool IsNotBusy => !IsBusy;
}
}
An ObservationViewModel
using SunObs.Maui.Services;
using SunObs.Maui.Models;
using SunObs.Maui.Views;
using System.Diagnostics;
using System.Collections.ObjectModel;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace SunObs.Maui.ViewModel
{
public partial class ObservationViewModel : BaseViewModel
{
[ObservableProperty]
ShortObs shortObs = new();
[ObservableProperty]
Observation observation;
ObservationService obsService;
public ObservationViewModel(ObservationService observationService)
{
Title = "Observations";
obsService = observationService;
}
[RelayCommand]
public void ComputeObs()
{
try
{
if (CheckInputs() == true)
{
Observation = new()
{
ProjDescription = ObsLocation.MyLocation.ProjDescription,
Latitude = (ObsLocation.MyLocation.Latitude),
Longitude = (ObsLocation.MyLocation.Longitude),
AlignmentAngle = (ObsLocation.MyLocation.Alignmentangle),
TimeZoneId = ObsLocation.MyLocation.Tzid,
WT = ShortObs.WT,
ObsDate = ShortObs.ObsDate,
HCirRO = ShortObs.HCirRO,
HCirSun = ShortObs.HCirSun,
ObsGyro = ShortObs.ObsGyro
};
// now calculate Azimuth
SunAzimuth.AzimuthCalculator.GetAzimuth(Observation);
};
}
catch (Exception ex)
{
Debug.Print("Error: ", ex.Message);
}
}
private bool CheckInputs()
{
// check for all valid inputs
if (Helpers.ValidAngle(ShortObs.HCirRO, "Any") == false)
{
App.Current.MainPage.DisplayAlert("HCirRO Error", "Invalid Angle", "OK");
return false;
}
if (Helpers.ValidAngle(ShortObs.HCirSun, "Any") == false)
{
App.Current.MainPage.DisplayAlert("HCirSun Error", "Invalid Angle", "OK");
return false;
}
if (Helpers.ValidAngle(ShortObs.ObsGyro, "Any") == false)
{
App.Current.MainPage.DisplayAlert("ObsGyro Error", "Invalid Angle", "OK");
return false;
}
if (TimeSpan.TryParse(ShortObs.WTstring, out TimeSpan result) == false)
{
App.Current.MainPage.DisplayAlert("ObsGyro Error", "Invalid Angle", "OK");
return false;
}
else
{
ShortObs.WT = result;
}
return true;
}
[RelayCommand]
void SavetoList()
{
obsService.AddObservation(Observation);
}
}
}
and the View
<?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"
x:Class="SunObs.Maui.Views.NewPage1"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
xmlns:converters="clr-namespace:SunObs.Maui.Converters"
xmlns:viewmodel="clr-namespace:SunObs.Maui.ViewModel"
x:DataType="viewmodel:ObservationViewModel"
Title="Observations">
<ContentPage.Resources>
<converters:AngleConverter x:Key="AngConv"/>
</ContentPage.Resources>
<VerticalStackLayout BackgroundColor="Transparent" Spacing="10" Margin="10,10,10,0">
<Grid RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto, Auto"
ColumnDefinitions="Auto,*" RowSpacing="5" ColumnSpacing="5" Margin="8">
<Frame Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource frameheader}">
<Label Style="{StaticResource lblheader}" Text="Observations"/>
</Frame>
<Frame Grid.Row="1" Grid.Column="0" Style="{StaticResource frameinput}">
<Label Style="{StaticResource lbl}" Text="Observation Id"/>
</Frame>
<Frame Grid.Row="2" Grid.Column="0" Style="{StaticResource frameinput}">
<Label Style="{StaticResource lbl}" Text="Observation Date"/>
</Frame>
<Frame Grid.Row="3" Grid.Column="0" Style="{StaticResource frameinput}">
<Label Style="{StaticResource lbl}" Text="Watch Time"/>
</Frame>
<Frame Grid.Row="4" Grid.Column="0" Style="{StaticResource frameinput}">
<Label Style="{StaticResource lbl}" Text="Horiz Angle to RO" ToolTipProperties.Text="Enter format = ddd.mmss"/>
</Frame>
<Frame Grid.Row="5" Grid.Column="0" Style="{StaticResource frameinput}">
<Label Style="{StaticResource lbl}" Text="Horiz Angle to Sun" ToolTipProperties.Text="Enter format = ddd.mmss"/>
</Frame>
<Frame Grid.Row="6" Grid.Column="0" Style="{StaticResource frameinput}">
<Label Style="{StaticResource lbl}" Text="Observed Gyro" ToolTipProperties.Text="Enter format = ddd.mmss"/>
</Frame>
<Button Text="Compute Azimuth" Grid.Row="7" Grid.Column="0" Grid.ColumnSpan="2" Command="{Binding ComputeObsCommand}"/>
<Frame Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="2" Style="{StaticResource frameheader}">
<Label Style="{StaticResource lblheader}" Text="Calculated Results"/>
</Frame>
<Frame Grid.Row="9" Grid.Column="0" Style="{StaticResource frameresult}">
<Label Style="{StaticResource lblresult}" Text="UT of Observation"/>
</Frame>
<Frame Grid.Row="10" Grid.Column="0" Style="{StaticResource frameresult}">
<Label Style="{StaticResource lblresult}" Text="Az to Sun"/>
</Frame>
<Frame Grid.Row="11" Grid.Column="0" Style="{StaticResource frameresult}">
<Label Style="{StaticResource lblresult}" Text="Az to RO"/>
</Frame>
<Frame Grid.Row="12" Grid.Column="0" Style="{StaticResource frameresult}">
<Label Style="{StaticResource lblresult}" Text="Calculated Gyro"/>
</Frame>
<Frame Grid.Row="13" Grid.Column="0" Style="{StaticResource frameresult}">
<Label Style="{StaticResource lblresult}" Text="Gyro C-O"/>
</Frame>
<DatePicker Grid.Row="2" Grid.Column="1"
x:Name="DPicker" Date="{Binding ShortObs.ObsDate}"
VerticalOptions="Center"
DatePicker.Format="dd-MMM-yyyy"/>
<Entry Placeholder="00:00:00" PlaceholderColor="Blue"
x:Name="WT" Text="{Binding ShortObs.WTstring}"
VerticalOptions="Center"
Grid.Row="3" Grid.Column="1">
<Entry.Behaviors>
<toolkit:SelectAllTextBehavior />
</Entry.Behaviors>
</Entry>
<Entry Placeholder="0.0000" PlaceholderColor="Blue"
x:Name="HCirRO" Keyboard="Numeric" Text="{Binding ShortObs.HCirRO}"
VerticalOptions="Center"
Grid.Row="4" Grid.Column="1">
<Entry.Behaviors>
<toolkit:NumericValidationBehavior
x:Name="HROvalidator"
Flags="ValidateOnValueChanged">
</toolkit:NumericValidationBehavior>
<toolkit:SelectAllTextBehavior />
</Entry.Behaviors>
</Entry>
<Entry Placeholder="0.0000" PlaceholderColor="Blue"
x:Name="HCirSun" Keyboard="Numeric" Text="{Binding ShortObs.HCirSun}"
VerticalOptions="Center"
Grid.Row="5" Grid.Column="1">
<Entry.Behaviors>
<toolkit:NumericValidationBehavior
x:Name="HSunvalidator"
Flags="ValidateOnValueChanged">
</toolkit:NumericValidationBehavior>
<toolkit:SelectAllTextBehavior />
</Entry.Behaviors>
</Entry>
<Entry Placeholder="0.0000" PlaceholderColor="Blue"
x:Name="ObsGyro" Keyboard="Numeric" Text="{Binding ShortObs.ObsGyro}"
VerticalOptions="Center"
Grid.Row="6" Grid.Column="1">
<Entry.Behaviors>
<toolkit:NumericValidationBehavior
x:Name="Gyrovalidator"
Flags="ValidateOnValueChanged">
</toolkit:NumericValidationBehavior>
<toolkit:SelectAllTextBehavior />
</Entry.Behaviors>
</Entry>
<Label Text="{Binding Observation.UTObs, StringFormat='{}{0:ddd dd MMM yyyy HH:mm:ss}'}"
Grid.Row="9" Grid.Column="1"
Style="{StaticResource lblresult}"/>
<Label x:Name="AzSun" Text="{Binding Observation.Azimuth, Converter={StaticResource AngConv}, StringFormat='{} {0:0.0000}'}"
Grid.Row="10" Grid.Column="1"
Style="{StaticResource lblresult}"/>
<Label x:Name="AzRO" Text="{Binding Observation.AzRO, Converter={StaticResource AngConv}, StringFormat='{} {0:0.0000}'}"
Grid.Row="11" Grid.Column="1"
Style="{StaticResource lblresult}"/>
<Label Grid.Row="12" Grid.Column="1"
x:Name="CalcGyro" Text="{Binding Observation.CalcGyro, Converter={StaticResource AngConv}, StringFormat='{} {0:0.0000}'}"
Style="{StaticResource lblresult}"/>
<Label Grid.Row="13" Grid.Column="1"
x:Name="CMinusO" Text="{Binding Observation.CMinusO, Converter={StaticResource AngConv}, StringFormat='{} {0:0.0000}'}"
Style="{StaticResource lblresult}"/>
</Grid>
<Button Text="Save to List" Command="{Binding SavetoListCommand}"/>
<Button Text="New Observation"/>
</VerticalStackLayout>
</ContentPage>
My problem is that the Binding for Observation.CMinusO displays and incorrect result in the View. However, if I use the SaveCommand to add the observation to my list and then view these results in a different view, the CMinusO value is shown correctly. In other words, everything computes as it should.
If I use the Hot Reload to change the view binding for CMinusO and then reset it, the correct value is displayed in my view. See below, the value -298.005 is incorrect
After resetting the Binding I get the correct value of 0.1409
I'm pretty sure the problem lies in my Observation model and the 3 calculated properties AzRO, CalcGyro and CMinusO but cannot figure how to solve it.
Should AzRO include a [NotifyPropertyChangedFor(nameof(CalcGyro))] - if so, how.
Similarly, should CalcGyro include a [NotifyPropertyChangedFor(nameof(CMinusO))], how?
TIA