Home > Software engineering >  How do I reset the binding from my source (ViewModel) to the target (.xaml)
How do I reset the binding from my source (ViewModel) to the target (.xaml)

Time:05-19

I have a ProfilePage.xaml containing some ImageButtons like these (six of them):

<ImageButton
  x:Name="resultImage"
  Source="{Binding Profile.Images[0].Source}"
  Command="{Binding HandleImage}"
  CommandParameter="0">

Which are binded through my ProfilePage.xaml.cs file to a Model called Profile.cs as follows:

BindingContext = new ViewModels.EditProfilePageViewModel(Profile);

Where Profile.cs holds the list of those images:

List<Image> images = new List<Image>();
public Profile()
{
    images.Add(new Image());
    images.Add(new Image());
    images.Add(new Image());
    images.Add(new Image());
    images.Add(new Image());
    images.Add(new Image());
}

The problem is that at some point I need to make sliding using the following code inside my ProfilePageViewModel.cs

for (int i = index; i < Profile.NumberOfImages - 1; i  )
{
    Profile.Images[i].Source = Profile.Images[i   1].Source;
}

This results into making List item to point at the next one and all together to the last. I tried to avoid it using new string() etc. but its useless because of the way those Profile.Images[i].Source are set using the following code:

var result = await MediaPicker.PickPhotoAsync();
if (result != null)
{
     var Text = $"File Name: {result.FileName}";
     if (result.FileName.EndsWith("jpg", StringComparison.OrdinalIgnoreCase) ||
            result.FileName.EndsWith("png", StringComparison.OrdinalIgnoreCase) ||
            result.FileName.EndsWith("jpeg", StringComparison.OrdinalIgnoreCase))
     {
            var stream = await result.OpenReadAsync();
            var source = ImageSource.FromStream(() => stream);
            // Add the 'Load Image Icon' on the first available cell
            Profile.Images[Profile.NumberOfImages].Source = source;
            // Increase the number of photos 
            Profile.NumberOfImages  ;
     }

I'm pretty sure that this way of loading images ruins the binding and creates this whole mess, as by hardcoding the values of these image sources doesn't cause any problem to the binding. So the question I'm making consists of two things. A better solution on how to Load images from camera roll for MAUI, without using FFImageLoading (which doesn't work anymore) OR a way to reset my binding.


UPDATE: After days of struggling I made the code way simple.

Profile.cs:

public class Profile : INotifyPropertyChanged
{


    public event PropertyChangedEventHandler PropertyChanged;
    /** --------------------------------------------------------------------
    *                           Properties
    *  --------------------------------------------------------------------
    */

    ObservableCollection<ImageSource> imageSources = new ObservableCollection<ImageSource>();
    public ObservableCollection<ImageSource> ImageSources
    {
        get { return imageSources; }
        set { imageSources = value; OnPropertyChanged(nameof(ImageSources)); }
    }
    public Profile()
    {
        
        ImageSources.Add("image1.jpg");
        ImageSources.Add("image2.jpg");
        ImageSources.Add("image3.jpg");
        ImageSources.Add("image4.jpg");
        ImageSources.Add("image5.jpg");
        ImageSources.Add("image6.jpg");
        ImageSources.Add("load_icon.jpg");
      } 
...

EditProfilePage.xaml:

    <ScrollView>
    <VerticalStackLayout>

        <!-- Photos Grid -->

        <Grid HorizontalOptions="Center" Padding="30,40" Margin="0,0" RowSpacing="10" ColumnSpacing="10">

            <Grid.RowDefinitions>
                <RowDefinition Height="90"></RowDefinition>
                <RowDefinition Height="90"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="60"></ColumnDefinition>
                <ColumnDefinition Width="60"></ColumnDefinition>
                <ColumnDefinition Width="60"></ColumnDefinition>
            </Grid.ColumnDefinitions>

            <!-- Image 1 -->

            <ImageButton
                    x:Name="resultImage"
                    Source="{Binding Profile.ImageSources[0]}"
                    Grid.Row="0"
                    Grid.Column="0"
                    Aspect="AspectFill"
                    Grid.RowSpan="1"
                    Grid.ColumnSpan="1"
                    Command="{Binding HandleImage}"
                    CommandParameter="0"

EditProfilePage.xaml.cs

 public partial class EditProfilePage : ContentPage
{
  public EditProfilePage(Profile Profile)
  {
    InitializeComponent();
    BindingContext = new 
    ViewModels.EditProfilePageViewModel(Profile);
    
  }
 }

EditProfileViewModel.cs:

    public EditProfilePageViewModel(Profile Profile)
    {
        this.Profile = Profile;
        HandleImage = new Command(OnHandleImage);
    }

    void OnPropertyChanged(string propertyName) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    void OnHandleImage(object CommandParameter)
    {
        // Get the index of the image
        int index = int.Parse(CommandParameter.ToString());

        // If the index of the selected image is less than the number of the images, delete the selected image
        if (index < NumberOfImages-1)
        {
            DeleteSelectedImage(index);
            
        }
        // In case the ImageButton tapped is the one after the last photo
        else if (index == NumberOfImages-1)
        {
            // Then add a new photo at that posiition
            LoadPhotoFromLibrary();
            
        }
        // In any other case
        else
        {
            // Just do nothing
            return;
        }

        for (int i=0; i<NumberOfImages; i  )
        {
            Console.WriteLine("Image: "   i   " Src:"   Profile.ImageSources[i]);
        }
    }

    void DeleteSelectedImage(int index)
    {
        Profile.ImageSources.RemoveAt(index);
        Profile.ImageSources = Profile.ImageSources;
        Console.WriteLine("Deleted: index"   index    " Number of Image: "   NumberOfImages);

    }
    async void LoadPhotoFromLibrary()
    {

        // Permsissions
        var status = await CheckAndRequestPhotosPermissionAsync();
        if (status != PermissionStatus.Granted)
        {
            // TODO:Notify user permission was denied
            return;
        }

        
        var result = await MediaPicker.PickPhotoAsync(new MediaPickerOptions
        {
            Title = "Please pick a photo"
        });
        var stream = await result.OpenReadAsync();

        Profile.ImageSources.Insert(NumberOfImages-1, ImageSource.FromStream(() => stream).ToString());
                    
        Profile.ImageSources = Profile.ImageSources;
        Console.WriteLine("Number of Image: "   NumberOfImages);   
    }

As of right now the DeleteSelecteImage() works fine with the hardcoded values. Whenever I load a new image from the simulator, the image is normally shown in the view but if I try to delete from that point of, whichever image I tap to delete, it deletes the last one.

I need to mention that the following error is shown when I upload an image that way from the library:

[unspecified] container_system_group_path_for_identifier: error = ((container_error_t)98) NOT_CODESIGNED [MC] Error getting system group container for systemgroup.com.apple.configurationprofiles: 98 [MC] Failed to get profile system group container path. Overriding with expected path: /Users/user/Library/Developer/CoreSimulator/Devices/5C228DCC-D446-423B-BE53-EFE37BDE02A6/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles objc[6750]: Class _PathPoint is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x121945650) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x16724d690). One of the two will be used. Which one is undefined. objc[6750]: Class _PointQueue is implemented in both /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore (0x121945628) and /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/TextInputUI.framework/TextInputUI (0x16724d6b8). One of the two will be used. Which one is undefined.

CodePudding user response:

Based on your Updated code, you can avoid the need to directly access individual elements ({Binding Profile.ImageSources[0]}), by using a CollectionView with Grid layout.

EditProfilePage.xaml:

<ScrollView>
    <CollectionView x:Name="myCollection" ItemsSource="{Binding Profile.ImageSources}"
                    ItemsLayout="VerticalGrid, 3"
                    WidthRequest="210" HeightRequest="210" HorizontalOptions="Center" >
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <Grid BackgroundColor="DarkBlue" Padding="5" >
                    <ImageButton Source="{Binding .}"
                                 Command="{Binding BindingContext.HandleImage, Source={x:Reference myCollection}}"
                                 CommandParameter="{Binding .}" />
                </Grid>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</ScrollView>

public class EditProfileViewModel
{
    ...
    void OnHandleImage(object CommandParameter)
    {
        // Get the index of the image
        var item = (ImageSource)CommandParameter;
        int index = Profile.IndexOf(item);
    }
}


public class Profile : ...
{
    public ObservableCollection<ImageSource> ImageSources { get; set; } = new ObservableCollection<ImageSource>();

    public int IndexOf(ImageSource item)
    {
        return ImageSources.IndexOf(item);
    }
}

EXPLANATION:

  • ItemsLayout="VerticalGrid, 3" arranges the 6 images in a grid. (Or ItemsLayout="HorizontalGrid, 2", for a different ordering of them.)
  • Profile.ImageSources is a collection of ImageSources.
  • Source={x:Reference myCollection} is used in an item binding, to access HandleImage property on EditProfileViewModel. (Which is passed from page down to the CollectionView.)
  • Source="{Binding .}" uses the item itself as the ImageSource.
  • CommandParameter="{Binding .}" passes the item itself (an ImageSource) as the parameter.
  • The index is found using IndexOf.
  • Related