Search code examples
c#xamlmaui

View element accessed in code-behind is null when it should be initialized - why?


I have three classes involved in my problem. PopupView, CardView, and TestPopup. PopupView represents a base class for popups, and uses CardView, to display 3 ContentView linerally with header body and footer. TestPopup inherits PopupView which itself exposes a Body property that sets the CardView Body property with the latter.

Please note that PopupView and CardView are in a separate class library, it seems to matter because if I define TestPopup in the class library everything works fine. So at this point I think either I am doing something horribly wrong or there is a bug maybe someone is aware of in MAUI.

Here is the code:

PopupText.xaml

   <tt:PopupView 
        xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:mct="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
        xmlns:tt="http://barambibol.io/dotnet/maui/timertoolkit"
        x:Class="TestApp.PopupTest"
        >
        <tt:PopupView.Body>
            <Label Text="AAA"/>
        </tt:PopupView.Body>
    </tt:PopupView>

PopupTest.Xaml.cs

public partial class PopupTest : PopupView
{
    public PopupTest()
    {
        InitializeComponent();
    }
}

PopupView (only relevant code provided)

public partial class PopupView : PopupBase
{
    public PopupView()
    {
        InitializeComponent();
    }

protected override void OnSizeAllocated(double width, double height)
{
    base.OnSizeAllocated(width, height);

    if (width != -1 && height != -1)
    {
        //-->>>>>>>>>>>> Here cardView is null. bodyContentView however is not null.
   

         cardView.WidthRequest = width * TimerToolkitConfig.PopupPageWidthRatio;
            cardView.MinimumHeightRequest = TimerToolkitConfig.ConfirmationPopupContentHeight;
            cardView.MaximumHeightRequest = height * TimerToolkitConfig.PopupPageHeightRatioMax;

            bodyContentView.Content = Body;
        }
    }
}

PopupView.xaml:

<popups:PopupBase 
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    x:Name="popupView"
                  >        
    <views:CardView x:Name="cardView" VerticalOptions="Center" HorizontalOptions="Center">
        <views:CardView.Header>
            <Grid>
                <Label 
                    HorizontalOptions="Center" VerticalOptions="Center"
                    Text="{Binding Source={x:Reference popupView}, Path=Title}"/>
            </Grid>
        </views:CardView.Header>

        <views:CardView.Body>
            <ContentView x:Name="bodyContentView"/>
        </views:CardView.Body>
    </views:CardView>
</popups:PopupBase>

PopupBase (PopupPage is from Mopups nuget):

public abstract class PopupBase : PopupPage
{
    protected TaskCompletionSource<PopupResult>? _taskCompletionSource;

    protected PopupResult ReturnValue { get; set; } = new PopupResult();

    public Task<PopupResult> PopupDismissedTask => _taskCompletionSource != null ? _taskCompletionSource!.Task : new TaskCompletionSource<PopupResult>().Task;

    public PopupBase()
    {
        CloseWhenBackgroundIsClicked = false;
        BackgroundClicked += PopupBase_BackgroundClicked;
    }

    protected virtual async Task Dismiss(bool cancelled = false)
    {
        ReturnValue.Cancelled = cancelled;

        await MopupService.Instance.PopAsync();
    }

    private async void PopupBase_BackgroundClicked(object? sender, EventArgs e)
    {
        await Dismiss(true);
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        _taskCompletionSource = new TaskCompletionSource<PopupResult>();
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        if (_taskCompletionSource != null)
        {
            _taskCompletionSource!.SetResult(ReturnValue);
        }
    }
}

    public sealed class PopupResult
    {
        public object? ReturnValue { get; set; }

        public bool Cancelled { get; set; }
    }

CardView.xaml.cs:

public partial class CardView : ContentView
{
    public static readonly BindableProperty HeaderProperty = BindableProperty.Create(nameof(Header), typeof(View), typeof(CardView));
    public View Header
    {
        get => (View)GetValue(HeaderProperty);
        set => SetValue(HeaderProperty, value);
    }

    public static readonly BindableProperty BodyProperty = BindableProperty.Create(nameof(Body), typeof(View), typeof(CardView));
    public View Body
    {
        get => (View)GetValue(BodyProperty);
        set => SetValue(BodyProperty, value);
    }

    public static readonly BindableProperty FooterProperty = BindableProperty.Create(nameof(Footer), typeof(View), typeof(CardView));
    public View Footer
    {
        get => (View)GetValue(FooterProperty);
        set => SetValue(FooterProperty, value);
    }

    public CardView()
    {
        InitializeComponent();
    }
}

CardView.xaml:

<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:TimerToolkit"
             x:Class="TimerToolkit.Views.CardView"
             x:Name="cardView"
             >
    <Border Style="{StaticResource CardStyle}" VerticalOptions="Start" x:Name="parentBorder">
        <Grid x:Name="grid" RowDefinitions="auto,*,auto">
            <ContentView 
                BackgroundColor="{x:Static local:TimerToolkitConfig.CardHeaderFooterColor}"
                Content="{Binding Source={x:Reference cardView}, Path=Header}"/>
            <ContentView 
                BackgroundColor="{x:Static local:TimerToolkitConfig.CardBodyColor}"
                Content="{Binding Source={x:Reference cardView}, Path=Body}" Grid.Row="1"/>
            <ContentView 
                BackgroundColor="{x:Static local:TimerToolkitConfig.CardHeaderFooterColor}"
                Content="{Binding Source={x:Reference cardView}, Path=Footer}" Grid.Row="2"/>
        </Grid>
    </Border>
</ContentView>

Here when I try to access an element from the view in code-behind, one of them is null. It is "cardView". "bodyContentView", which is a child of "cardView", is not null. How is that even possible ? Any light on this would be greatly appreciated I honestyl have absolutely no clue how this can happen

EDIT: this code is defined in PopupView.xaml.cs and was not included in the original post

public static readonly BindableProperty BodyProperty = BindableProperty.Create(nameof(Body), typeof(View), typeof(PopupView));
public View Body
{
    get => (View)GetValue(BodyProperty);
    set => SetValue(BodyProperty, value);
}

Solution

  • I reproduced your problem in your sample and a new project. And what I found:

    1. When you create sub class of the PopupView in the main project and display it, the sub-class's content(cardview) will be null even though it's children is not null.
    2. The cardView will not be null, if you change the CardView in the PopupView to some basic control such as:
    <popups:PopupBase 
        xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        x:Name="popupView"
                      >        
        <VerticalStackLayout x:Name="cardView" VerticalOptions="Center" HorizontalOptions="Center">
                <Grid>
                    <Label 
                        HorizontalOptions="Center" VerticalOptions="Center"
                        Text="{Binding Source={x:Reference popupView}, Path=Title}"/>
                </Grid>
    
                <ContentView x:Name="bodyContentView"/>
        </VerticalStackLayout>
    </popups:PopupBase>
    
    1. This problem not only appears on the MoPopups, I tested with content page: Create a ContentPage to use the CardView in the class library -> Declare a sub contentpage implement it in the main project -> Display the suc contentpage. And the cardview is null.

    So the problem is the reference of custom control in the class library gets mistake when you create sub class to implement the class library's class that uses the custom control.

    You can report this as an new issue on the repo. And for a workaround, just declare the sub class in the class library instead of the main project.