Maui view does not update Binding value correctly

John Rutherford 21 Reputation points
2023-12-01T05:54:27.94+00:00

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

User's image

After resetting the Binding I get the correct value of 0.1409

User's image

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

.NET MAUI
.NET MAUI
A Microsoft open-source framework for building native device applications spanning mobile, tablet, and desktop.
2,900 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. John Rutherford 21 Reputation points
    2023-12-02T04:53:29.5433333+00:00

    Solved it myself by including [NofiyPropertyChangedFor] attribute to Azimuth property.

    0 comments No comments