Xamarin - Call controller method from an external ViewModel

73 Views Asked by At

I am not very familiar with Xamarin and I am not sure how to solve my current scenario, in a nutshell, I don't know how to call my method SaveSignature from outside the controller - below there is a bit of an explanation of the flow (I have stripped out some of the code for simplicity)

I have this controller SignaturePadController.xaml:

<?xml version="1.0" encoding="UTF-8"?>
<Frame
    xmlns:signature="clr-namespace:SignaturePad.Forms;assembly=SignaturePad.Forms" 
    xmlns:lang="clr-namespace:Mobile.Business.Extensions;assembly=Mobile.Business"
    x:Class="Mobile.UI.Controls.SignaturePadControl"
    x:Name="this">

  <StackLayout>
    <signature:SignaturePadView 
        x:Name="SignaturePad"
        x:FieldModifier="Public"/>
        
        <controls:LabelEntryLayout x:Name="HiddenRecipientSignature" Value="{Binding RecipientSignature}" IsVisible="false"/>
  </StackLayout>
</Frame>

SignaturePadController.xaml.cs:

namespace Mobile.UI.Controls
{
  public partial class SignaturePadControl : Frame
  {
    public SignaturePadControl()
    {
        InitializeComponent();
    }

    public async void SaveSignature(object sender, EventArgs e)
    {
        var base64String = string.Empty;

        using (var stream = AsyncHandler.RunSync(async () => await SignaturePad.GetImageStreamAsync(SignatureImageFormat.Png)))
        {
            if (stream == null) return;

            var bytes = new byte[stream.Length];
            stream.Read(bytes, 0, (int)stream.Length);
            
            HiddenRecipientSignature.Value = Convert.ToBase64String(bytes);
        }
    }
  }
}

Which I am using inside a view called BatchReviewPage like this:

<controls:SignaturePadControl 
  x:Name="SignaturePadControl"/>

The view model for the above view is called BatchReviewViewModel and it has the property that will contain the signature once saved:

private string _recipientSignature;
public string RecipientSignature
{
    get => _recipientSignature;
    set => SetAndRaiseIfChanged(ref _recipientSignature, value);
}

Now, I need to call the SaveSignature method from another view model called TargetViewModel - how do I do that?

This is the piece of code where I need to call the SaveSignature method:

var batchReviewVm = await ShowPopup<BatchReviewViewModel>();
if (batchReviewVm == null) return;

batchReviewVm.OnPopupClose = async () =>
{
    switch (batchReviewVm.SelectedCloseOption)
    {
        case Batch.BatchViewModelBase<TrackerBatchItemModel>.CloseOption.Confirm:
            //NEED TO CALL SaveSignature
        break;
    default:
        break;
    }
};

I'll explain a bit the code above.

I have a list of items, as soon as I click on one of them, I open the popup BatchReviewPage.xaml, I fill up the provided form and add a signature, as soon as Confirm is clicked I need to Save/Retrive the signature.

Thank you for your help

Comment replies: I do agree 100% with you @Jason, I know that is just wrong currently.

I have now created a bindable property on the SignaturePadControl, this is the library I am using https://github.com/xamarin/SignaturePad.

private static readonly Type _thisType = typeof(SignaturePadControl);

public static BindableProperty SignatureProperty =
BindableProperty.Create(
    propertyName: nameof(Signature),
    returnType: typeof(string),
    declaringType: _thisType,
    defaultValue: null,
    defaultBindingMode: BindingMode.OneWay);

public string Signature
{
  get => (string)GetValue(SignatureProperty);
  set => SetValue(SignatureProperty, value);
}

This is how I use the controller:

<controls:SignaturePadControl 
IsVisible="{Binding ShowRecipientSignaturePad}"
IsOkayVisible="True"
Signature="{Binding RecipientSignature}"/>

I am still very much struggling to understand how to trigger the save signature, I'd like to do it on the BatchReviewViewModel but can't figure out how to tell my code that it is time to save the signature.

I have tried to add properties to signature:SignaturePadView, like Focused, Unfocused, etc...to maybe trigger an event once the user has added the signature?! but that did nothing.

Thanks for you help

@Liqun Shen-MSFT, Yes it is and with that button inside the controller it works but it needs to be removed, that's why I am trying to trigger the save differently and in a different file.

2

There are 2 best solutions below

0
Daniele On

In the end, I found a way to trigger the SaveSignature although it is not ideal.

I have managed to find 2 properties on the SignaturePadView to trigger when the User insert or clear a signature (StrokeCompleted and Cleared).

This is what my SignaturePadControl.xaml looks like:

<signature:SignaturePadView 
x:Name="SignaturePad"
x:FieldModifier="Public"
BackgroundColor="#FAFAD2" 
CaptionText="" 
ClearText="Clear" 
ClearTextColor="#B8860B" 
CaptionTextColor="#B8860B" 
HeightRequest="300" 
HorizontalOptions="FillAndExpand"
PromptText="{lang:Translate SIGNATURE}"
PromptTextColor="#B8860B"
SignatureLineColor="#B8860B" 
StrokeColor="Black" 
StrokeWidth="5" 
StrokeCompleted="SaveSignature"
Cleared="ClearSignature"
VerticalOptions="CenterAndExpand" />

<controls:LabelEntryLayout x:Name="HiddenRecipientSignature" Value="{Binding RecipientSignature}" IsVisible="false"/>

And this is the code behind:

public void SaveSignature(object sender, EventArgs e)
{
  var base64String = string.Empty;

  using (var stream = AsyncHandler.RunSync(async () => await SignaturePad.GetImageStreamAsync(SignatureImageFormat.Png)))
  {
    if (stream == null) return;

    var bytes = new byte[stream.Length];
    stream.Read(bytes, 0, (int)stream.Length);
    HiddenRecipientSignature.Value = Convert.ToBase64String(bytes);
  }
}

public void ClearSignature(object sender, EventArgs e)
{
   HiddenRecipientSignature.Value = null;
}

My BatchReviewViewModel stays the same:

private string _recipientSignature;
public string RecipientSignature
{
  get => _recipientSignature;
  set => SetAndRaiseIfChanged(ref _recipientSignature, value);
}

This works for me, and I'll keep it as it is for now.

I have tried to use BindebleProperty as suggested, but I failed.

This is the BindableProperty I had created but I felt lost since I wasn't sure how to proceed:

private static readonly Type _thisType = typeof(SignaturePadControl);

public static BindableProperty SignatureProperty =
BindableProperty.Create(
    propertyName: nameof(Signature),
    returnType: typeof(string),
    declaringType: _thisType,
    defaultValue: null,
    defaultBindingMode: BindingMode.OneWay);

public string Signature
{
    get => (string)GetValue(SignatureProperty);
    set => SetValue(SignatureProperty, value);
}

Was I supposed to bind Signature to StrokeCompleted? Who knows.

Thank you for all the suggestions.

2
Liqun Shen-MSFT On

From your code, I assume you want to save your signature to RecipientSignature Property when the Popup (BatchReviewPage) closes. But I am a bit confused about your code. You use a custom control called LabelEntryLayout and get the stream from signaturePad. Though you set IsVisible to false. Then why use use it?

As an alternative, I would like to use a BindableProperty and Detect property changes.

In SignaturePadControl.cs

    public static readonly BindableProperty NeedSaveProperty =BindableProperty.Create("NeedSave", typeof(bool), typeof(SignaturePadControl), false,propertyChanged:OnNeedSavePropertyChanged);

    public bool NeedSave
    {
        get { return (bool)GetValue(NeedSaveProperty); }
        set { SetValue(NeedSaveProperty, value); }
    }

    private static async void OnNeedSavePropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var ob = bindable as SignaturePadControl;
        using (var stream = await ob.SignaturePad.GetImageStreamAsync(SignatureImageFormat.Png))
        {
            if (stream == null) return;

            var bytes = new byte[stream.Length];
            stream.Read(bytes, 0, (int)stream.Length);

           var viewModel = ob.BindingContext as BatchReviewViewModel;
            viewModel.RecipientSignature = Convert.ToBase64String(bytes);
        }
    }

In the above code, we first create a BindableProperty called NeedSave. Then register a property-changed callback method. The callback method will be invoked when the value of the NeedSave changes.

In BatchReviewPage, we consume the custom control,

We can easily bind the NeedSave BindableProperty to a Property in BatchReviewViewModel.

<controls:SignaturePadControl  ...
    NeedSave="{Binding NeedSaveFlag}"/>

So in BatchReviewViewModel, we define the Property,

private bool needSaveFlag = false;
public bool NeedSaveFlag
{
    get
    {
        return needSaveFlag;
    }
    set
    {
        needSaveFlag = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NeedSaveFlag)));
    }
}

Okay, suppose when we are at TargetViewModel, we just get the BatchReviewViewModel instance and set the NeedValueFlag to true. Then OnNeedSavePropertyChanged will be invoked and Signature string will be saved.

var viewModel = page.BindingContext as BatchReviewViewModel;
viewModel.NeedSaveFlag = true;

Hope it helps!