JaneySprings / DotNet.Meteor

A VSCode extension that can run and debug .NET apps (Xamarin, MAUI, Avalonia)
https://marketplace.visualstudio.com/items?itemName=nromanov.dotnet-meteor
MIT License
268 stars 10 forks source link

[Hot Reload] BindingContext in the XAML file resets after document reload #83

Closed jpmock closed 7 months ago

jpmock commented 7 months ago

OS: Mac

Hot reload does work but only works on very basic layouts. With anything remotely complex like items in a listview, it does not work, it actually causes the UI on the page to be all out of whack.

JaneySprings commented 7 months ago

Hi @jpmock! HotReload also works with CollectionView. Check the following demo:

https://github.com/JaneySprings/DotNet.Meteor/assets/48021947/bedb307c-2ab3-4e71-a9a1-c04bb2f9cb1f

Maybe you have an example to reproduce? It will be helpful

jpmock commented 7 months ago

Here is one of my more basic pages which has a CollectionView:

`<?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:local="clr-namespace:MyApp.Portable" BindingContext="{Binding WorkOrderDetailViewModel, Source={StaticResource ViewModelLocator}}" x:Class="MyApp.Portable.WorkOrderDetailPage" xmlns:zx="clr-namespace:ZXing.Net.Maui.Controls;assembly=ZXing.Net.MAUI.Controls" xmlns:behaviors="clr-namespace:MyApp.Portable.Behaviors;assembly=MyApp.PortableCore">

` Here is how it looks before a simple update to the parent layout BackgroundColor: ![Screenshot 2024-02-29 at 11 10 46 AM](https://github.com/JaneySprings/DotNet.Meteor/assets/8386433/c8fc9946-efed-4243-9ef7-b391ea0dd40f) I have scratched out some personal information in red. When I update the BackGroundColor to red and hit save, I get this: ![Screenshot 2024-02-29 at 11 11 05 AM](https://github.com/JaneySprings/DotNet.Meteor/assets/8386433/d65d7d1b-cfc9-4bfe-bad7-9d3dcf1b3dc1) If I navigate away and come back to this page, it loads normally as if I never changed anything in the first place, as shown in the first image. Strange behavior.
jpmock commented 7 months ago

Wow, the code view above took all the nice formatting out of the xaml, sorry. If you put it into an IDE and click format it will show you better.

I appreciate you looking into this!. Let me know if you need more detail.

JaneySprings commented 7 months ago

Can you try to set binding context from code behind (.xaml.cs file) and remove this line:

BindingContext="{Binding WorkOrderDetailViewModel, Source={StaticResource ViewModelLocator}}"
jpmock commented 7 months ago

@JaneySprings thank you for the suggestion! This works! We will certainly do this workaround to each page while we need hot reload and as long as it won't work using the ViewModelLocator. The ViewModelLocator gives us a couple things mainly, dependency injection and no code in the codebehind, but we will certainly give that up to have a working hot reload.

Thank you!!!!

JaneySprings commented 7 months ago

No problem! I will check this behavior with a debugger, maybe I can fix it

JaneySprings commented 7 months ago

Hi, @jpmock! I tried to create a simple example for this bug and I can't reproduce it. For example:

My view model and locator:

public class VMLocator {
    public MyViewModel MyViewModel { get; }

    public VMLocator() {
        MyViewModel = new MyViewModel();
    }
}

public class MyViewModel {
    public string Text { get; set; }

    public MyViewModel() {
        Text = "Hello, Maui!";
    }
}

App.xaml:

<Application.Resources>
    <local:VMLocator x:Key="VMLocator" />
</Application.Resources>

MainPage.xaml:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiFlyoutApp"
             x:Class="MauiFlyoutApp.MainPage"
             BindingContext="{Binding MyViewModel, Source={StaticResource VMLocator}}">

    <VerticalStackLayout>
        <Label Text="{Binding Text}" />
    </VerticalStackLayout>

</ContentPage>

Do you have any example project for this bug?

jedlimke commented 7 months ago

I'm on a Mac and am experiencing this behavior. I may be able to provide the example you seek, @JaneySprings.

I created a blank .NET MAUI app in an empty folder via the dotnet CLI as follows:

dotnet new install Microsoft.Maui.Templates.net8::8.0.7
dotnet new maui -n AlohaWorld -o .

This basic app, as generated by the template, is a "Hello, World" style app with a simple button whose label is updated with every click.

When debugging against an iOS simulator (17.2), I can confirm that, upon launch, clicking the button increments the counter and updates the label as expected.

However, when updates are made to the XAML for the MainPage, the connection seems to become partially severed. The page does, indeed, update to reflect whatever changes I make to the XAML (changing the headline label, for example), but after that, clicking the button and firing the OnCounterClicked event handler no longer results in button-label updates.

I can set a breakpoint in the OnCounterClicked handler and see that the count variable is incrementing as expected from the code-behind, but the button label, itself, can no longer be updated by setting CounterBtn.Text.

This may not be exactly what @jpmock is experiencing, but I have a feeling it's pretty closely related and may have the same solution.

launch.json:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": ".NET Meteor Debugger",
      "type": "dotnet-meteor.debugger",
      "request": "launch",
      "preLaunchTask": "dotnet-meteor: Build"
    }
  ]
}

MainPage.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"
             x:Class="AlohaWorld.MainPage">

    <ScrollView>
        <VerticalStackLayout
            Padding="30,0"
            Spacing="25">
            <Image
                Source="dotnet_bot.png"
                HeightRequest="185"
                Aspect="AspectFit"
                SemanticProperties.Description="dot net bot in a race car number eight" />

            <Label
                Text="Aloha, World!"
                Style="{StaticResource Headline}"
                SemanticProperties.HeadingLevel="Level1" />

            <Label
                Text="Welcome to &#10;.NET Multi-platform App UI"
                Style="{StaticResource SubHeadline}"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Welcome to dot net Multi platform App U I" />

            <Button
                x:Name="CounterBtn"
                Text="Click me" 
                SemanticProperties.Hint="Counts the number of times you click"
                Clicked="OnCounterClicked"
                HorizontalOptions="Fill" />
        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

MainPage.xaml.cs:

namespace AlohaWorld;

public partial class MainPage : ContentPage
{
    int count = 0;

    public MainPage()
    {
        InitializeComponent();
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);
    }
}

MauiProgram.cs:

using Microsoft.Extensions.Logging;
using DotNet.Meteor.HotReload.Plugin;

namespace AlohaWorld;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

#if DEBUG
        builder.EnableHotReload();
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

AlohaWorld.csproj:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFrameworks>net8.0-android;net8.0-ios;net8.0-maccatalyst</TargetFrameworks>
        <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>

        <OutputType>Exe</OutputType>
        <RootNamespace>AlohaWorld</RootNamespace>
        <UseMaui>true</UseMaui>
        <SingleProject>true</SingleProject>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>

        <!-- Display name -->
        <ApplicationTitle>AlohaWorld</ApplicationTitle>

        <!-- App Identifier -->
        <ApplicationId>com.companyname.alohaworld</ApplicationId>

        <!-- Versions -->
        <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
        <ApplicationVersion>1</ApplicationVersion>

        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
        <TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
    </PropertyGroup>

    <ItemGroup>
        <!-- App Icon -->
        <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />

        <!-- Splash Screen -->
        <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />

        <!-- Images -->
        <MauiImage Include="Resources\Images\*" />
        <MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />

        <!-- Custom Fonts -->
        <MauiFont Include="Resources\Fonts\*" />

        <!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
        <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
        <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
        <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
        <PackageReference Include="DotNetMeteor.HotReload.Plugin" Version="3.*"/>
    </ItemGroup>

</Project>
JaneySprings commented 7 months ago

Hi, @jedlimke ! Thank you for the example. I know about this bug and I will try to fix it on the next weekend

Upd: I can fix this bug! This is not a best solution but it's better than nothing 😉

https://github.com/JaneySprings/DotNet.Meteor/assets/48021947/2a3d9c65-0ff9-482f-95f5-a0e9185026d5

JaneySprings commented 7 months ago

Hi, @jedlimke ! Try to update .NET Meteor to the 4.2.0 version and check this bug again.

jpmock commented 7 months ago

@JaneySprings is this regarding the ViewmodelLocator bug or specific to @jedlimke issue? We will upgrade and try it anyway. Thanks

JaneySprings commented 7 months ago

I think it's affected only @jedlimke issue. In your case there is something strange with ViewmodelLocator after a page reload. If you can reproduce it with the simple app, please, let me know.

jedlimke commented 7 months ago

@JaneySprings Hello!

I updated to version 3.2.0 and can confirm that things are fixed for the issue I reported!

I assume that it is expected behavior, however, that I need to cause data to refresh in order to update the UI. That is, after a hot reload occurs, everything looks "broken" as before, but once I interact with the UI and cause data to change that's bound to the UI, the UI then reflects the correct data.

I've attached a video demonstrating the new behavior.

  1. I launch the unchanged app
  2. I interact with two independent buttons that track how often they've been clicked independently. (4 clicks green, 2 clicks violet)
  3. Then, I edit the XAML off screen and update the heading, causing the UI to reload. NOTE that both of the buttons have reverted to their natural unclicked state.
  4. Then I interact with one, then the other, to return them to where they should be.

Is this expected behavior? It's certainly better than before.

https://github.com/JaneySprings/DotNet.Meteor/assets/2490879/baed229d-e645-4609-be78-1d960bd16962

JaneySprings commented 7 months ago

Yes, it’s expected behavior, because a element (Button) resets all of its properties to the state, defined in the XAML file after reload.

Maybe there is a better solution (copy a state of the old element to the new element) but I can’t do it right now 🤔