I have been struggling with this for days and here I finally am to ask the community:
I have a .NET MAUI project that involves a page containing a BindableLayout in a StackLayout. The source data for the BindableLayout is a ContentView component that I am referencing from elsewhere in the project. The data in the viewmodel is loading in fine, everything is as the documentation says it should be. It’s just not loading the content when the page loads.
Here’s the super fun part:
If, while debugging with hot reload enabled, I backspace and replace the ‘.’ in the line
<reusableViews:BathroomView Content="{Binding .}"/>
the content then loads and everything carries on as I want it to.
Here is my xaml for the parent page:
<Grid>
<Grid.RowDefinitions>
<!-- Row 0: Fixed height for the button -->
<RowDefinition Height="*" />
<!-- Row 1: Remaining space for the ScrollView -->
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Row 1: ScrollView -->
<ScrollView
x:Name="ScrollView"
Grid.Row="0"
VerticalOptions="FillAndExpand">
<StackLayout>
<!-- Display the BathroomView instances -->
<StackLayout x:Name="BathroomsLayout" BindableLayout.ItemsSource="{Binding BathroomViews}">
<BindableLayout.ItemTemplate>
<DataTemplate>
<!-- Directly render the BathroomView -->
<reusableViews:BathroomView Content="{Binding .}"/>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
<StackLayout x:Name="ScrollReference" />
</StackLayout>
</ScrollView>
<AbsoluteLayout Grid.Row="0">
<customControls:LoadingFrame x:Name="NewBathroomLoadingFrame" IsVisible="{Binding IsBusy}" />
</AbsoluteLayout>
<!-- Row 0: Button -->
<Border Grid.Row="1" Margin="0,10,0,10">
<StackLayout Margin="0,10,0,10">
<reusableViews:AdditionalCommentsView />
<Button
Clicked="AddNewBathroom_Clicked"
HorizontalOptions="Center"
Text="Add New Bathroom"
VerticalOptions="Start" />
<Button
x:Name="BathroomsPageSubmitButton"
Clicked="BathroomsPageSubmitButton_Clicked"
Text="Submit Bathrooms Page" />
</StackLayout>
</Border>
</Grid>
cs for the parent page:
public partial class BathroomsPage
{
private PageMaster _pageMaster;
private PageFunctions _pageFunctions;
private bool _navigated;
public BathroomsPage(PageMaster pageMaster, PageFunctions pageFunctions)
{
_pageMaster = pageMaster;
_pageFunctions = pageFunctions;
InitializeComponent();
BindingContext ??= _pageMaster.BathroomsPageViewModel;
}
}
pageMaster and pageFunctions are singletons and are behaving fine.
Here is my BathroomView xaml:
<ContentView
x:Class="..."
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:buttons="http://schemas.syncfusion.com/maui"
xmlns:customControls="clr-namespace:..."
xmlns:maui="clr-namespace:FFImageLoading.Maui;assembly=FFImageLoading.Maui"
xmlns:reusableViews="clr-namespace:...">
<ContentView.Resources>
<ResourceDictionary>
<Style TargetType="HorizontalStackLayout">
<Setter Property="HorizontalOptions" Value="Center" />
</Style>
<Style TargetType="customControls:ErrorLabel">
<Setter Property="Margin" Value="0,10,0,0" />
</Style>
</ResourceDictionary>
</ContentView.Resources>
<ContentView.Content>
<Frame>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<buttons:SfExpander
x:Name="BathroomsExpander"
Margin="0,15,0,15"
AnimationDuration="150"
ClassId="BathroomsExpander"
HorizontalOptions="Center"
IsExpanded="False"
Expanded="BathroomsExpander_Expanded">
<buttons:SfExpander.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="35" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<HorizontalStackLayout HorizontalOptions="Center">
<Image
Margin="40,0,2,2"
Source="Resources/Images/bath.png"
VerticalOptions="Center"
WidthRequest="40" />
<Label
Grid.Column="1"
Margin="40,0,0,0"
CharacterSpacing="0.25"
FontAttributes="Bold"
FontFamily="Roboto-Regular"
FontSize="14"
HorizontalTextAlignment="Center"
Text="{Binding Bathroom.BathroomName}"
VerticalOptions="CenterAndExpand" />
</HorizontalStackLayout>
</Grid>
</buttons:SfExpander.Header>
<buttons:SfExpander.Content>
<StackLayout Margin="0,10,0,0">
<StackLayout
Margin="0,0,20,0"
HorizontalOptions="Center"
Orientation="Horizontal">
<Label Text="Bathroom Name: " />
<Entry
x:Name="BathOne"
HorizontalTextAlignment="Center"
Text="{Binding Bathroom.BathroomName}"
WidthRequest="175" />
<!--<ImageButton
x:Name="RemoveButton"
Command="{Binding ViewModel.ClearNameCommand}"
CommandParameter="{Binding Bathroom}"
HeightRequest="30"
Source="Resources/Images/remove.png"
WidthRequest="15" />-->
</StackLayout>
<customControls:ErrorLabel Name="BathroomNameError" Text="Please give a name to this bathroom" />
<Label
FontAttributes="Bold"
HorizontalTextAlignment="Center"
Text="GFCI Outlets?" />
<StackLayout>
<HorizontalStackLayout>
<RadioButton Content="Yes" IsChecked="{Binding Bathroom.GfciOutletsYes}" />
<RadioButton Content="No" IsChecked="{Binding Bathroom.GfciOutletsNo}" />
<RadioButton Content="No Outlet" IsChecked="{Binding Bathroom.GfciNoOutlet}" />
</HorizontalStackLayout>
<StackLayout x:Name="GfciFuncNonFuncLayout" IsVisible="{Binding Bathroom.GfciOutletsYes}">
<HorizontalStackLayout HorizontalOptions="Center">
<RadioButton Content="Functional" IsChecked="{Binding Bathroom.GfciFunc}" />
<RadioButton Content="Non-Functional" IsChecked="{Binding Bathroom.GfciNonFunc}" />
</HorizontalStackLayout>
<StackLayout HorizontalOptions="Center" IsVisible="{Binding Bathroom.GfciNonFunc}">
<Label HorizontalTextAlignment="Center" Text="Please describe the issue with the outlet:" />
<Entry Text="{Binding Bathroom.GfciNonFuncEntry}" />
<customControls:ErrorLabel Name="GfciNonFuncEntryError" Text="Please detail the problem with the outlet" />
</StackLayout>
<customControls:ErrorLabel Name="GfciFuncNonFuncError" Text="Please select functional or non-functional" />
</StackLayout>
<customControls:ErrorLabel Name="GfciYesNoNoOutletError" Text="Please select an option for the GFCI outlets" />
</StackLayout>
<StackLayout Margin="40,0,40,0">
<Border
Margin="{OnPlatform Default='8,0,8,8',
WinUI='8,0,6,8',
MacCatalyst='8,0,6,8'}"
Stroke="#CAC4D0"
StrokeShape="RoundRectangle 8,8,8,8"
StrokeThickness="{OnPlatform MacCatalyst=2,
Default=1}"
WidthRequest="{OnPlatform MacCatalyst=460,
WinUI=340}">
<buttons:SfExpander AnimationDuration="150">
<buttons:SfExpander.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="35" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image
Margin="14,2,2,2"
Source="Resources/Images/commentplus.png"
VerticalOptions="Center" />
<Label
Grid.Column="1"
Margin="10,0,0,0"
CharacterSpacing="0.25"
FontFamily="Roboto-Regular"
FontSize="14"
Text="Additional Comments"
VerticalOptions="CenterAndExpand" />
</Grid>
</buttons:SfExpander.Header>
<buttons:SfExpander.Content>
<Editor
Margin="20"
HeightRequest="200"
Placeholder="Enter any additional info here"
Text="{Binding Bathroom.AdditionalCommentsText}" />
</buttons:SfExpander.Content>
</buttons:SfExpander>
</Border>
<Border>
<StackLayout Margin="0,20,0,20">
<HorizontalStackLayout>
<Image
Margin="10"
Source="Resources/Images/imagesicon.png"
WidthRequest="30" />
<Label
FontAttributes="Bold"
FontSize="Large"
HorizontalTextAlignment="Center"
Text="Images" />
</HorizontalStackLayout>
<!-- UPDATE NAME OF BUTTON -->
<Button
x:Name="AddBathroomPhotoButton"
Clicked="AddBathroomPhotoButton_Clicked"
Text="Add Photo"
WidthRequest="150" />
<StackLayout x:Name="ImagesLayout" />
</StackLayout>
</Border>
</StackLayout>
</StackLayout>
</buttons:SfExpander.Content>
</buttons:SfExpander>
<StackLayout Grid.Row="1">
<Button
Command="{Binding ViewModel.RemoveCommand}"
CommandParameter="{Binding Bathroom}"
Text="Remove Bathroom" />
</StackLayout>
</Grid>
</Frame>
</ContentView.Content>
</ContentView>
And the BathroomView.xaml.cs:
using ECHI_Inspection_App_Maui.CustomControls;
using ECHI_Inspection_App_Maui.Helpers;
using Echi_Inspection_App_Maui.Models;
using Echi_Inspection_App_Maui.ViewModels.InspectionPageViewModels;
using Microsoft.Maui.Controls.Compatibility;
using Newtonsoft.Json;
using SQLite;
using Guid = System.Guid;
using FFImageLoading.Maui;
using StackLayout = Microsoft.Maui.Controls.StackLayout;
using System.Collections.ObjectModel;
namespace Echi_Inspection_App_Maui.Reusable_Views;
public partial class BathroomView
{
public static readonly BindableProperty ViewModelProperty =
BindableProperty.Create(nameof(ViewModel), typeof(BathroomsPageViewModel), typeof(BathroomView));
public static readonly BindableProperty BathroomModelProperty =
BindableProperty.Create(nameof(Bathroom), typeof(Bathroom), typeof(BathroomView));
public BathroomsPageViewModel ViewModel
{
get => (BathroomsPageViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
public Bathroom Bathroom
{
get => (Bathroom)GetValue(BathroomModelProperty);
set => SetValue(BathroomModelProperty, value);
}
public BathroomView()
{
InitializeComponent();
}
private void DeletePhotoButton_Clicked(object? sender, EventArgs e)
{
if (sender is not Button button) return;
// Get the parent StackLayout that contains both the CachedImage and the Button
if (button.Parent is not StackLayout parentLayout) return;
// Find the CachedImage within the parent StackLayout
if (parentLayout.Children.FirstOrDefault(c => c is CachedImage) is not CachedImage cachedImage) return;
// Remove the image source from your data collection
var imageUrl = cachedImage.Source?.ToString();
if (imageUrl != null)
{
Bathroom.Images.Remove(imageUrl);
// Perform any additional cleanup or logic related to the image removal
PageFunctions.DeletePhoto(imageUrl);
}
// Remove the CachedImage and the Button from the parent StackLayout
parentLayout.Children.Remove(cachedImage);
parentLayout.Children.Remove(button);
}
private async void AddBathroomPhotoButton_Clicked(object sender, EventArgs e)
{
var uploadedUrl = await PageFunctions.CapturePhotoAndGetUrl($"Interior WCF {Guid.NewGuid()}.jpg");
if (uploadedUrl == null) return;
Bathroom.Images.Add(uploadedUrl); //UPDATE THIS
var deleteButton = new Button()
{
Text = "Delete",
WidthRequest = 100,
ClassId = "ImageButton",
};
deleteButton.Clicked += DeletePhotoButton_Clicked;
ImagesLayout.Children.Add(new StackLayout()
{
Children =
{
new CachedImage()
{
Source = uploadedUrl,
Aspect = Aspect.Center,
WidthRequest = 200
},
deleteButton
}
});
}
private static void AddImagesToStackLayout(StackLayout layout, ObservableCollection<string> images)
{
foreach (var image in images)
{
layout.Children.Add(new StackLayout()
{
Children =
{
new CachedImage()
{
Source = image,
Aspect = Aspect.Center,
WidthRequest = 200
},
new Button()
{
Text = "Delete",
WidthRequest = 100,
ClassId = "ImageButton"
}
}
});
}
}
private void BathroomsExpander_Expanded(object sender, Syncfusion.Maui.Expander.ExpandedAndCollapsedEventArgs e)
{
Bathroom.Images ??= [];
if (!Bathroom.Images.Any()) return; //UPDATE THIS
AddImagesToStackLayout(ImagesLayout, Bathroom.Images);
var imageButtons = this.GetVisualTreeDescendants().OfType<Button>().Where(x => x.ClassId == "ImageButton")
.ToList();
foreach (var imageButton in imageButtons)
{
imageButton.Clicked += DeletePhotoButton_Clicked;
}
}
}
BathroomsExpander_Expanded() is there as an attempt to load the images at the bottom of the ContentView because of another problem that I won’t get into here.
Just to reiterate the fun part: I can delete and replace the ‘.’ in
<reusableViews:BathroomView Content="{Binding .}"/>
and everything renders fine. I am down to thinking that that is the biggest clue. It either has something to do with the order of rendering vs data, it’s a bug in the framework, or I’m just not using it correctly.
Should have just gone with Angular
2
First of all, BathroomView
? Coolest name for a control ever!
<reusableViews:BathroomView Content=”{Binding .}”/>
Here you creating a new instance of BathroomView
and setting its content to item of your ItemsSource, which is again an object of BathroomView
.
This is not ideal. You can define a new ContentView
instead and set it’s Content
to you BathroomView
object. Like below
<DataTemplate>
<!-- Directly render the BathroomView -->
<ContentView Content="{Binding .}"/>
</DataTemplate>
2