Forum Xamarin.Forms

Is it possible to force a page to revalidate or reload it's self from a remote function?

JKnottJKnott Member ✭✭✭

My app has a popup display that asks a user to select a photo, or take a photo.
The popup takes some incoming arguments that specify the parent record information.
When the camera function takes the photo it stores the image in the database along with the parent record data so it can be recovered later.

Here's the problem.
The PopupView is non awaitable. So when the user gets the popup to select camera vs existing picture, the code has already continued along, so I cannot simply make the page refresh it's record from the DB after the photo has been taken.

I am pretty sure I am seriously overthinking this (I have a tendency to do this). I saw that there is an external lib I can NuGet AwaitablePopups, it overloads RG.Plugins.Popup. But I am trying to limit my use of external libs, I already have a dozen or more from AdMob, to SyncFusion elements, etc. I know they are more or less a commonplace for OOP now, and the larger projects are typically quite stable.

I guess this boils down to opinion, and preference, but what would be the best method to use in other peoples opinion?

Tagged:

Answers

  • LeonLuLeonLu Member, Xamarin Team Xamurai

    You can use MVVM to change it, bind a property, then binding the property to the <Image Source="{Binding Image}"></Image>.

    And the property must achieve the INotifyPropertyChanged interface like following format.

     // public string Image { get; set; }
            string image;
            public string Image
            {
                set
                {
                    if (image != value)
                    {
                        image = value;
                        OnPropertyChanged("Image");
    
                    }
                }
                get
                {
                    return image;
                }
            }
    
     public event PropertyChangedEventHandler PropertyChanged;
    
            protected virtual void OnPropertyChanged(string propertyName)
            {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
    

    Then, read the image from gallery or take a photo. set the source directly. Image=" ".

    If you still cannot achieve it, please share your demo or plugin link that I can produce this issue.

  • JKnottJKnott Member ✭✭✭
    edited February 22

    @LeonLu That's what I have more or less, it's just a matter of as we used to call it. "Race Condition Code".

    Here's the basic break down...

    public static async Task SelectCameraOption(Photos p)
    {
      App.Current.Resources.TryGetValue("icoFileImage", out object iconFile);
      App.Current.Resources.TryGetValue("icoCamera", out object iconCamera);
      popupMessage = new SfPopupLayout
      {
        PopupView =
        {
           AppearanceMode = AppearanceMode.OneButton,
           AutoSizeMode = AutoSizeMode.Both,
    
           HorizontalOptions = LayoutOptions.CenterAndExpand,
           AnimationEasing = AnimationEasing.SinIn,
           AnimationMode = AnimationMode.Fade,
           AnimationDuration = 400,
    
           ShowCloseButton = false,
           ShowFooter = false,
           ShowHeader = false,
        }
      };
    
      var layout = new StackLayout
      {
         Orientation = StackOrientation.Horizontal,
         VerticalOptions = LayoutOptions.CenterAndExpand,
         HorizontalOptions = LayoutOptions.CenterAndExpand,
         Margin = new Thickness(40, 10),
      };
    
      var slFileCol = new StackLayout
      {
         Orientation = StackOrientation.Vertical,
         VerticalOptions = LayoutOptions.CenterAndExpand,
         HorizontalOptions = LayoutOptions.CenterAndExpand,
      };
    
      var slCameraCol = new StackLayout
      {
         Orientation = StackOrientation.Vertical,
         VerticalOptions = LayoutOptions.CenterAndExpand,
         HorizontalOptions = LayoutOptions.CenterAndExpand,
      };
    
      var ibFile = new ImageButton
      {
         Command = new Command(
                             execute: () => { GetPictureFromFile(p); popupMessage.Dismiss(); },
                             canExecute: () => true),
         VerticalOptions = LayoutOptions.CenterAndExpand,
         HorizontalOptions = LayoutOptions.CenterAndExpand,
         Source = iconFile.ToString(),
         WidthRequest = 105,
         Aspect = Aspect.AspectFit,
         Padding = 15,
      };
    
      var lbFile = new Label
      {
         Text = StringTools.GetStringResource("szGetImageFromFile"),
         HorizontalTextAlignment = TextAlignment.Center,
         HorizontalOptions = LayoutOptions.CenterAndExpand,
         LineBreakMode = LineBreakMode.WordWrap,
         WidthRequest = 90,
      };
    
      slFileCol.Children.Add(ibFile);
      slFileCol.Children.Add(lbFile);
    
      var ibCamera = new ImageButton
      {
         Command = new Command(
                             execute: () => { GetPictureFromCamera(p); popupMessage.Dismiss(); },
                             canExecute: () => true),
         VerticalOptions = LayoutOptions.CenterAndExpand,
         HorizontalOptions = LayoutOptions.CenterAndExpand,
         Source = iconCamera.ToString(),
         WidthRequest = 105,
         Aspect = Aspect.AspectFit,
         Padding = 15,
      };
    
      var lbCamera = new Label
      {
         Text = StringTools.GetStringResource("szGetImageFromCamera"),
         HorizontalTextAlignment = TextAlignment.Center,
         HorizontalOptions = LayoutOptions.CenterAndExpand,
         LineBreakMode = LineBreakMode.WordWrap,
         WidthRequest = 90,
      };
    
      slCameraCol.Children.Add(ibCamera);
      slCameraCol.Children.Add(lbCamera);
    
      layout.Children.Add(slFileCol);
      layout.Children.Add(slCameraCol);
    
      popupMessage.PopupView.ContentTemplate = new DataTemplate(() => layout);
    
      popupMessage.Show(false);
    
      return;
    }
    

    Here is the actual Camera code it's self

    public static async Task<Photos> GetPictureFromCamera(Photos P)
    {
       await Semaphore.WaitAsync().ConfigureAwait(true)
       try
       {
           await CrossMedia.Current.Initialize().ConfigureAwait(true);
    
           // Check for the permissions to the camera, and attempt to request access if needed.
           var status = await CrossPermissions.Current.CheckPermissionStatusAsync<CameraPermission>().ConfigureAwait(true);
           if (status != PermissionStatus.Granted)
           {
               if (await CrossPermissions.Current.ShouldShowRequestPermissionRationaleAsync(Permission.Camera).ConfigureAwait(true))
               {
                   await Application.Current.MainPage.DisplayAlert(StringTools.GetStringResource("szCameraPermissionHeader"),
                                                                       StringTools.GetStringResource("szAllowAccessToCamera"),
                                                                       StringTools.GetStringResource("szOK")).ConfigureAwait(true);
                   status = await CrossPermissions.Current.RequestPermissionAsync<CameraPermission>().ConfigureAwait(true);
               }
           }
    
           // We got permission to take the picture, so let's do it.... (what a worthless comment.....)
           if (status == PermissionStatus.Granted)
           {
               // Create the connections, initialize the camera, and make sure we can take a Photo
               if (!CrossMedia.Current.IsCameraAvailable || !CrossMedia.Current.IsTakePhotoSupported)
               {
                   await Application.Current.MainPage.DisplayAlert(StringTools.GetStringResource("szErrorNoCameraHeader"),
                                                                   StringTools.GetStringResource("szErrorNoCamera"),
                                                                   StringTools.GetStringResource("szOK")).ConfigureAwait(true);
                   return null;
               }
    
               //Need to make this a relative path for the photo to be stored at.
               var loacalsavePath = await DependencyService.Get<ISave>().CreateSavePath(Path.Combine(StringTools.GetStringResource("szImagesFolder"), GetFolderByCat(P.RecordCategory))).ConfigureAwait(false);
               if (!System.IO.Directory.Exists(loacalsavePath))
               {
                   throw new Exception("Save Path non existent");
               }
    
               var documentPath = Path.Combine(StringTools.GetStringResource("szImagesFolder"), GetFolderByCat(P.RecordCategory));
    
               P.FileName = StringTools.MakeValidFileName(P.FileName);
    
               var newname = string.Empty;
    
               for (int n = 0; n < 98; n++)
               {
                   newname = $"{P.FileName}-{n}.jpg".Trim();
                   // If the file does not exist, great, lets make it!
                   if (!File.Exists(Path.Combine(loacalsavePath, newname))) 
                   {
                       break;
                   }
               }
    
               // Check that we have access to be playing with these files.
               var storragestatus = await CrossPermissions.Current.CheckPermissionStatusAsync<StoragePermission>().ConfigureAwait(true);
               if (storragestatus != PermissionStatus.Granted)
               {
                   // Prompt the user that permissions are not set yet.
                   if (await CrossPermissions.Current.ShouldShowRequestPermissionRationaleAsync(Permission.Storage).ConfigureAwait(true))
                   {
                       await Application.Current.MainPage.DisplayAlert("File Access", StringTools.GetStringResource("szAccessFiles"), StringTools.GetStringResource("szOK")).ConfigureAwait(true);
                       // Request the permissions from the system again.
                       storragestatus = await CrossPermissions.Current.RequestPermissionAsync<StoragePermission>().ConfigureAwait(true);
                   }
               }
    
               // We got permission to access the files, so let's do it.... (what a worthless comment.....)
               if (storragestatus == PermissionStatus.Granted)
               {
                   var saveImageas = Path.Combine(documentPath, newname);
    
                   // Take the actual Photo
                   var file = await CrossMedia.Current.TakePhotoAsync
                       (new StoreCameraMediaOptions
                       {
                           Directory = "./" + documentPath,
                           Name = newname,
                           SaveToAlbum = false,
                           CompressionQuality = 95,
                           CustomPhotoSize = 95,
                           PhotoSize = PhotoSize.MaxWidthHeight,
                           MaxWidthHeight = 1600,
                           DefaultCamera = CameraDevice.Front,
                           ModalPresentationStyle = MediaPickerModalPresentationStyle.FullScreen,
                       });
    
                   // If we didn't get a picture, just bounce out from here
                   if (file == null)
                   {
                       return null;
                   }
    
                   try
                   {
                       try
                       {
                           File.Copy(file.Path, Path.Combine(DependencyService.Get<ISave>().GetSavePath(), saveImageas));
                       }
                       catch (Exception ex)
                       {
                           DebugTools.LogException(ex);
                       }
    
                       using (var memoryStream = new MemoryStream())
                       {
                           file.GetStream().CopyTo(memoryStream);
                           var myfile = memoryStream.ToArray();
                           P.Image = myfile;
                           P.Path = saveImageas;
                           P.RecordCategory = P.RecordCategory;
                           P.ParentRecord = P.ParentRecord;
                           P.RecordType = P.RecordType;
                           P.FileName = Path.GetFileNameWithoutExtension(saveImageas);
    
                           await new PhotosRepository().AddItem(P).ConfigureAwait(false);
                       }
    
                       //Check for the file, if the settings say delete it, do so.
                       if (File.Exists(file.Path) && !AppSettingsFunctions.GetBoolSetting("app_SavePictureFiles"))
                       {
                           File.Delete(file.Path);
                       }
                   }
                   catch (Exception ex)
                   {
                       DebugTools.LogException(ex);
                   }
               }
               else if (status != PermissionStatus.Unknown)
               {
                   await Application.Current.MainPage.DisplayAlert("Camera Denied", StringTools.GetStringResource("CameraAccessDenied"), StringTools.GetStringResource("szOK")).ConfigureAwait(true);
               }
           }
       }
       catch (Exception ex)
       {
           await Application.Current.MainPage.DisplayAlert(StringTools.GetStringResource("szError"), ex.Message, StringTools.GetStringResource("szOK")).ConfigureAwait(true);
           DebugTools.LogException(ex); ;
       }
       finally
       {
           Semaphore.Release();
       }
       return P;
    }
    

    Then my page that call this code, this is called as the OnClicked event for an image button on the page in the app.

    private async void TakePhoto(object sender, EventArgs e)
    {
      try
      {
          // check if valid to take Photo.
          if (ValidatePhotoData())
          {
              return;
          }
    
          // check if casing is -1, if it is save the record and continue.
          if (viewModel.MyAmmo.Id == -1)
          {
              SaveData();
          }
    
          // if the casing id is still -1 return and start over.
          if (viewModel.MyAmmo.Id == -1)
          {
              return;
          }
    
          var newFileName = StringTools.MakeValidFileName($"{Manufacturer.Text}-{Model.Text}-{Chamber.Text}");
          var pic = new Photos
          {
              ParentRecord = viewModel.MyAmmo.Id,
              FileName = newFileName,
              RecordCategory = (int)RecordCatagory.Ammo,
              RecordType = (int)RecordType.Bullets,
          };
    
          var n = await GetPhotoFromCamera.SelectCameraOption(pic);
          //This is actually pointless since the code returns immediately
          if (n != null)
          {
              viewModel.PhotosList.Add(n);
          }
      }
      catch (Exception ex)
      {
          DebugTools.LogException(ex);
      }
    }
    

    The ObservableCollection that is the list of photos is this.

    public ObservableCollection<Photos> PhotosList
    {
        get => _photoslist ?? new ObservableCollection<Photos>();
    
        set
        {
            SetProperty(ref _photoslist, value, nameof(PhotosList));
            OnPropertyChanged(nameof(PhotosList));
        }
    }
    

    I have overloaded the SetProperty and OnPropertyChangedevents like this

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
      var changed = PropertyChanged;
      if (changed == null)
      {
          return;
      }
      changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    
    protected bool SetProperty<T>(ref T backingStore, T value, [CallerMemberName] string propertyName = "", Action onChanged = null)
      {
        if (EqualityComparer<T>.Default.Equals(backingStore, value))
        {
            return false;
        }
        backingStore = value;
        onChanged?.Invoke();
        OnPropertyChanged(propertyName);
        return true;
      }
    

    The problem rests in the fact that when the picture choosing/taking code is executed, it is on a different thread, in a different context.

    I see only 2 practical ways to correct this...

    1. Pass the actual ObservableCollection to the camera code by reference. I could then use the built-in .Add(item) to add the image to the display after having added it to the database. This would then execute the OnPropertyChanged for the actual list, which should hopefully cause the element to update and reflect the change.
    2. I could create a accessor function in the viewModel that would force a refresh of the database. This function would use a globally accessible SemaphoreSlim to wait to refresh the data. I would then call the release for the semaphore in the camera code.

    I think either of these would work, with a preference on the second option. Since the object is being passed by reference I would not be as worried about pushing truckloads of data around in memory.

    Any ideas or opinions would be greatly appreciated!

    Cheers!

  • JKnottJKnott Member ✭✭✭

    LOL Option 1 sounds great, until you realize that you can't pass ref objects to an async function...
    Guess option 2 is looking more and more appealing.

  • JKnottJKnott Member ✭✭✭

    @LeonLu Let me know if my logic is accurate here if you would.

    If I want to make the function I was talking about, I would do use the following code.

    internal static class Globals
    {
        internal static SemaphoreSlim dbSemaphore;
    }
    

    then within App (aka Main())

    //By initializing the semaphore with 0 it will immediately block when Wait() is called?
    Globals. = new SemaphoreSlim(0);
    

    then in my ViewModel I could do this.

        private async void SayCheese()
        {
            try
            {
        //This will immediately block the thread and wait for the semaphore
                await CoreGlobals.dbSemaphore.WaitAsync();
    
        //This function causes the collection of pictures to refresh from the database
                await InitPhotos;
            }
            catch (Exception ex)
            {
                DebugTools.LogException(ex);
            }
        }
    

    When I go to take the picture I would do the following.

    private async void TakePictureClicked()
    {
        ......
        viewModel.SayCheese();
    
        GetCamera.TakePicureUsingCamera();
    

    . ....
    }

    Finally in my GetCamera.TakePicureUsingCamera();

    public static async Task TakePicureUsingCamera()
    {
        ....
        // a  bunch of camera picture taking code....
        CoreGlobals.dbSemaphore.Release();
        ....
    }
    

    I'm not entirely sure about my semaphore logic here.

    Cheers!

  • JKnottJKnott Member ✭✭✭

    From the research I've done so far, it seems that my biggest problem has to do with needing to work with instance data externally on another thread. Every article I read and all of my examples fail because I need to use Static classes and objects. But Static items cannot work with instance data from what I can tell.
    They work great for background functions, but when it comes to working with an instance of data on a page, they don't seem to be able to do what I need.

    For instance, I cannot pass my ObservableCollection by reference to my camera code, because the code is async functions cannot accept ref objects.

    To create a blocking Semaphore to wait for signal to update the instance of my ObservableCollection I will have to create it using something like Thread t = new Thread( functiontorun); but the problem is that functiontorun has to be static, so once again it cannot work with instance data on the page.

    I am trying to make my code as reusable as possible. I have about 18 different pages that will need to be able to take pictures, so I am using a Static function that creates a PopupView that asks the user if they want to take a picture, or select an existing image in their gallery.

    The PopupView cannot be awaited, so the code will run off and exit the current function before the popup is even shown. Since a static object cannot take instance data, I need an external method to call in order to cause the instance data to refresh.

    In C++ I would create a loop on a separate thread that watches a Semaphore or perhaps even a Mutex that I would then signal from within my Static code.

    I cant seem to figure out a way to make this work using Xamarin (or perhaps just C#).

    I am thinking I will need to basically create an entire new page that will be pushed to the Navigation stack that takes the ref to the previous pages Image collection. This page could then proceed through all of the async calls on it's thread, using the ref object as an instance object of it's own.

    I would prefer the method I'm using now (mainly because I already have the code in place but am not pleased with the side effects it has).
    As for what these side effects are?
    When the user selects adding an image, an instance object is created and passed to the called function. This object has pertinent data to be stored in the database with the image (parent record PK, Category, Type, Filename to use, etc).
    Once the image is taken by the camera, since it is on it's own thread, in a Static function, it will submit the image to the database. but the page that called it, never knows that the database has been updated.

    Anyhow, I hope this makes sense, or at least explains how my code is NOT making sense at the moment.
    I also hope there is a way I can do this without having to re-engineer my entire image taking subsystem.

    All that being said, I think I will be creating a new label in my Git repository, and working on doing this revamping of the subsystem.

    Thank you so much for your help! If nothing else I think being able to post on the forum is helping me sort my thoughts out and come up with alternate ideas for how to approach this and other problems.

    Cheers!

  • LeonLuLeonLu Member, Xamarin Team Xamurai

    I notice you used Syncfusion's SfPopupLayout, this is a third part plugin, you can open a thread in the Syncfusion forum about this issue.

  • JKnottJKnott Member ✭✭✭

    I've been looking into using another 3rd party plugin I've found AwaitablePopup It's based on RG.Plugin.Popup. the problem I have with that one is that there is sparse to little documentation for it.

    My idea of using the global Semaphore is working. The problem now being that the control that consumes my OvservableCollection is not updating when the collection changes, and there doesn't seem to be any sort of refresh command for the control. The problematic display control is another SyncFusion control SfCarousel. I've got a message to them on that issue.

    It's odd, I have the OnPropertyChanged event set for my collection, and I've even tried calling it manually both my overloaded version as well as `base.OnPropertyChanged' None of them seem to update the control. Again this is a Syncfusion issue, so I have them looking into it.

    In the end the global Semaphore worked, so I will post the code that I used to implement it, and mark this fixed.

    Thank you again for your help! The ability to talk to a person and work the issue, rather than spin it around in my head has been immeasurably helpful!

    Cheers!

  • JKnottJKnott Member ✭✭✭

    As promised here is the code I used to get my desired functionality. The control on my display still is not updating, but I believe this to be a problem in the control I am using to display my data.

    In the viewModel class for the page I've added this code.
    InventoryViewModel.cs

      internal async void SayCheese()
      {
          await WaitToRefreshPhotoList();
      }
    
      internal async Task WaitToRefreshPhotoList()
      {
          await CoreGlobals.dbSemaphore.WaitAsync();
          await RefreshPhotos().ConfigureAwait(true);
      }
    

    Then in the implementation code you would add this where you take the picture.
    MyViewPage.xaml.cs

    internal void TakePicture_Clicked()
    {
        try
        {
          .... Code to setup or prepare to take the picture...
          MyPhotoClass pic = new MyPhotoClass(
              { 
                ParentRecord = viewModel.MyInventory.Id, 
                Category = (int)enuCategories.Inventory
              });
    
          viewModel.SayCheese();
          await GetPhotoFromCamera.SelectCameraOption(pic);
        }
        catch (Exception ex)
        {
          DebugTools.LogException(ex);
        }
    
    }
    

    When in my code I use a popup display to choose between taking a photo, and selecting one from the gallery.
    Since this code is inconsequential to this, I wont include it, but here is the code inside of the camera function to trigger the SemaphoreSlim
    CameraTakePhoto.cs

      Public static Task TakePhotoFomCamera()
      {
        TryGetValue
        {
          ....Code to take photo....
        }
        catch (Exception ex)
        {
          DebugTools.LogException(ex); ;
        }
        finally
        {
          CoreGlobals.dbSemaphore.Release();
        }
        return;
      }
    

    Finally, I greate a global class called CoreGlobals, then set a globally accessible SemaphoreSlim.
    ApplicationGlobals.cs

    internal static class CoreGlobals
    {
      static SemaphoreSlim dbSemaphore = new SemaphoreSlim(0, 1);
    }
    

    The process this code follows is simple.

    User clicks button to add image to the database.
    1.The image class is initialized with the record info.
    2.The function to refresh the image collection is called
    * This function then creates a new thread and waits on a semaphore.
    3.Code to take the photo is executed.
    * At the end of the camera code the semaphore calls Release() this iterates the SemaphoreSlim.
    4.Now that the semaphore has been Released, it can execute the code to refresh the Image collection.

    I am presently still having trouble where the control that consumes this collection is not updating, but I believe this to be a problem with the control so I have sent the creator of the control a thread on this problem.

    Hope this can help people down the road!

    Cheers!

Sign In or Register to comment.