MAUI - OnPropertyChange not updating the UI

2.7k views Asked by At

Hi guys I’m working on a MAUI app and I have a problem with the ui not updating every after calling OnPRopertyChanged, here are the details:

When calling a command in the ViewModel from the imageButton in the XAML file (using binding), the command calls a method in the ViewModel that changes the source of the image and then OnPropertyChange is called and the image changes in the UI.

the approach above is not good enough because we are not allowing the user to choose between taking a photo and uploading an existing one.

so the other option is that the imageButton in the XAML file will call an event in the code behind there it will show a popup for choosing whether he wants to upload an existing photo or take a new photo. then the code behind will call the view model to update the new photo.

in this case when we call OnPrpertyChange the changes are not updating in our MAUI app. what are we doing wrong here?

this is the XAML file: the first option I was describing is implemented here with the first ImmageButton and the second approach is implemented in the second ImageButton

<Grid Grid.Row="1" 
                 HeightRequest="100"
                  ColumnDefinitions=",,," HorizontalOptions="FillAndExpand" >
                <Frame CornerRadius="13"   BorderColor="#c6c1ea"    HasShadow="False" Grid.Column="0" Margin="3" Padding="0">
                    <ImageButton ClassId="1" Source="{Binding ImageSource1, Mode=TwoWay}"  Aspect="AspectFill" Command="{Binding AddPhotoFromGallery_Clicked}" />
                </Frame>
                <Frame CornerRadius="13"  BorderColor="#c6c1ea" HasShadow="False" Grid.Column="1" Margin="3"  Padding="0">
                    <ImageButton ClassId="2" Source="{Binding ImageSource2, Mode=TwoWay}"   Aspect="AspectFill" Clicked="AddPhoto_Tapped" />
                </Frame>
            </Grid>

in the second option, we go through the code behind

private async void AddPhoto_Tapped(object sender, EventArgs e)
    {
        string choosedOption = await DisplayActionSheet("Choose option:", "Cancel", null, "From Gallery", "Take a photo");
    
        if(choosedOption == "From Gallery")
        {
            await (BindingContext as AddItemPageViewModel).AddPhotoFromGallery();
        }
        else if(choosedOption == "Take a photo")
        {
            await (BindingContext as AddItemPageViewModel).TakeAPhoto();
        }
    }

this is the ViewModel:

public Command AddPhotoFromGallery_Clicked
        => new Command(async () => await AddPhotoFromGallery());
        
         public async Task AddPhotoFromGallery()
        {
           
            var result = await MediaPicker.PickPhotoAsync(new MediaPickerOptions { Title = "Please pick a photo" });
            if (result != null)
            {
                var stream = await result.OpenReadAsync();
                ImageSource1 = ImageSource.FromStream(() => stream);
                OnPropertyChanged(nameof(ImageSource1));
               
            }
        }

Edit: This is how the properties are declared:


public ImageSource imageSource1;

public ImageSource ImageSource1
        {
            get { return imageSource1; }
            set
            {
                imageSource1 = value;
                OnPropertyChanged(nameof(ImageSource1));
            }
        }

1

There are 1 answers

3
ToolmakerSteve On

UPDATE

I made a simple example with the essential part of the original code in question. It worked for me on Windows. (I didn't need to revise the code, as shown in my answers.)

BUT NOT on Android 10 or 11 - even though I had added Permissions (and <queries> for Android 11) as described in Xamarin.Essentials: Media Picker. AFAIK, Maui MediaPicker should work the same.

Unfortunately, my answers don't work on Android either.

So the below answers might be moot.

I'm not sure what is wrong.


MODIFIED ANSWER

According to doc linked above, can't rely on result.FullPath; need to use OpenReadAsync and copy to a local file.

if (result != null)
{
    // save the file into local storage
    var newFile = Path.Combine(FileSystem.CacheDirectory, result.FileName);
    using (var stream = await result.OpenReadAsync())
    using (var newStream = File.OpenWrite(newFile))
        await stream.CopyToAsync(newStream);

    ImageSource1 = newFile;
}
<Image Source="{Binding ImageSource1}" WidthRequest="300" HeightRequest="200" />
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CAMERA" />
    <queries>
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
    </queries>
</manifest>

ORIGINAL ANSWER

To be safe, the stream should be opened inside the FromStream action.
If you do it earlier, then (on some platforms) it may be closed at the time the action runs - the action doesn't run at the instant the enclosing statement is executed. It runs slightly later, when Xamarin applies the image source to the Image.

() => stream

I did not find a way to perform result.OpenReadAsync inside of ImageSource.FromStream. (Actually I did, but it used Task.Result, which is not safe to do on UI thread - could deadlock the app.)

The easiest way to fix this is to NOT use ImageSource.FromStream. Instead, use ImageSource.FromFile:

if (result != null)
{
    ImageSource1 = ImageSource.FromFile(result.FullPath);
}