ReactiveUI - Invoke delegate in view on view model property change - reactiveui

I'm just getting started with ReactiveUI and working on a small WPF drawing app. The drawing library I'm using (SkiaSharp) doesn't support binding an image to the XAML element, instead you invoke the InvalidateVisual method of the XAML element which fires a PaintSurface event that you respond to in order to draw the image.
So what I'm trying to do is observe a property of the view model and when the property changes invoke the InvalidateVisual method.
I've attempted to WhenAnyValue to observe a property of the view model and Do to invoke the InvalidateVisual method by setting up a subscription in the view's constructor thusly
this.WhenActivated(disposable =>
{
… // property bindings
this.WhenAnyValue(x => ViewModel.SomeProperty)
.Do(x => DrawingSpace.InvalidateVisual())
.Subscribe()
.DisposeWith(disposable);
});
However this throws a System.NotSupportedException Unsupported expression type 'Constant'
What's the correct way to setup a subscription in a view that observes a view model property and invokes a delegate when the property changes?

The following is your issue:
this.WhenAnyValue(x => ViewModel.SomeProperty)
The WhenAnyValue needs a expression based on the x value passed in.
So in this case the correct approach would be to
this.WhenAnyValue(x => x.ViewModel.SomeProperty)

Related

Why is Xamarin DatePicker Binding causing a navigation failure?

I am using Prism and Autofac with Xamarin.Forms 4.0 with an MVVM architecture. Using the Navigation.NavigateAsync("MyPage") works unless I have a binding to the Date object with my ViewModel.
The page renders properly and I am navigated to it if my DatePicker has no binding.
<DatePicker x:Name="ProcessStartDate" Format="D" MinimumDate="01/01/2000" />
However the following will cause me to never navigate to the page.
<DatePicker x:Name="ProcessStartDate" Format="D" MinimumDate="01/01/2000" Date="{Binding SelectedStartDate, Mode=TwoWay}"
The property in the View Model, MyVM, looks like this.
private DateTime selectedStartDate;
public DateTime SelectedStartDate
{
get
{
return selectedStartDate;
}
set
{
SetProperty(ref selectedStartDate, value);
sample.ProcessStartDate = value;
}
}
Navigation with the following code fails with the Binding in XAML above:
INavigationResult status;
try
{
var parameters = new NavigationParameters();
parameters.Add("CurrentSample", SelectedSample);
status = await NavigationService.NavigateAsync("MyPage", parameters); //MyPage is registered with MyVM
}
catch (Exception ex)
{
string mess = ex.Message;
}
My work-around is to add an event handler to the code-behind.
<DatePicker x:Name="ProcessStartDate" Format="D" MinimumDate="01/01/2000" DateSelected="OnStartDateSelected"
So now my code-behind has a handler:
void OnStartDateSelected(object sender, DateChangedEventArgs args)
{
SampleDetailsViewModel vm = BindingContext as SampleDetailsViewModel;
vm.SelectedStartDate = args.NewDate;
}
I have a work-around for this page, But I don't want put code in the code-behind. This breaks the MVVM standard that I've managed to maintain on the other seven pages of the app. Am I Binding improperly with the DatePicker?
When Binding SelectedStartDate, you are not initializing it, making it binding to a null, because you have set the Binding Mode to "TwoWay".
Here you can find the various types of binding modes, quoting:
Causes changes to either the source property or the target property to
automatically update the other. This type of binding is appropriate
for editable forms or other fully-interactive UI scenarios.
a solution would be something like this (if you wanna keep the TwoWay mode, and dont mind starting with an default selected):
private DateTime selectedStartDate = DateTime.Now;
Or
Making the binding mode to "OneWayToSource", this makes updates to the binding source without, and not the target (remember that this way you can't change the selected date from the binding, only the datepicker can).
Updates the source property when the target property changes.
Or
If you wanna keep the TwoWay Mode and not having a default date selected, the way you did with code behind is a nice workaround.

How to redraw ZXing QR code on TextChanged Event - Xamarin

I'd like to redraw my QR Code every time a particular TextChanged event is triggered.
The ZXingBarcodeImageView object gets drawn in the view when the page loads with the value BarcodeValue set in the XAML file like this:
<forms:ZXingBarcodeImageView
Margin="5,5,5,0"
x:Name="QRCodeView"
BarcodeFormat="QR_CODE"
BarcodeValue="-1" //this is the value of the QR code
/>
I have an Entry with a TextChanged event attached, which triggers a function UpdateQRLabel. This function should redraw the QRCode with the new value in Entry
<Entry
x:Name="Message"
TextChanged="UpdateQRLabel"
/>
If I change the BarcodeValue parameter after the QRCode has been drawn, it DOES NOT get redrawn automatically.
I need to force the ZXingBarcodeImageView object to redraw every time the TextChanged event is triggered.
Question
How do I force the ZXingBarcodeImageView to redraw when the TextChanged event is triggered?
I'm not sure if you're using data-binding or not. Since you are using events I guess not, however, I did get it to work with data-binding. A sample repo can be found here: https://github.com/jfversluis/ZXingValueBinding
It comes down to this. Create a property which will hold your barcode value:
private string _barcodeValue = "-1";
public string BarcodeValue
{
get { return _barcodeValue; }
set
{
_barcodeValue = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BarcodeValue)));
}
}
The object holding this property needs to implement the INotifyPropertyChanged interface. You could consider using PropertyChanged.Fody for these.
I have put this property in the code-behind of my page, this can also be a separate class. Now, change your barcode image view to this: <forms:ZXingBarcodeImageView ... BarcodeValue="{Binding BarcodeValue}">.
Whenever you set a new value to BarcodeValue, the value should change because the UI is notified because of the INotifyPropertyChanged mechanism.

FFImageLoading : load into an ImageButton

In Xamarin.Android, to load an image using FFImageLoading, a ImageViewAsync must be used instead of a standard ImageView.
I couldn't find what I can use if I wanna load my image into an ImageButton. Didn't find an "ImageButtonAsync".
Dont know if this is relevant or not. I gave my svg a tap gesture to get it working like a button.
first reference the FFImageLoading.Svg.Forms in your xaml
namespace:FFImageLoading.Svg.Forms;assembly=FFImageLoading.Svg.Forms"
second add the svgImage with a tap gesture
<ffimageloadingsvg:SvgCachedImage Source="YourImageSource" >
<ffimageloadingsvg:SvgCachedImage.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding YourCommand}"/>
</ffimageloadingsvg:SvgCachedImage.GestureRecognizers>
</ffimageloadingsvg:SvgCachedImage>
You will call this in the constructor of your ViewModel to initialize the command
YourCommand= new DelegateCommand(async () => await ExecuteYourCommandAsync());
Create the properties that bind to the xaml file
public ICommand YourCommand
{
get => yourCommand;
set => SetProperty(ref yourCommand, value);
}
private ICommand yourCommand;
The await ExecuteYourCommandAsync is a method that you create. and in there you will put your logic of what you actually want the tap gesture to do.
You can also pass through object with the command. let met know if this makes sense
AFAIK there is no particular ImageButton to use with FFImageLoading but you can use directly an ImageViewAsync set it as android:clickable="true" and add the click event.
And as stated here an ImageButton is just an ImageView that has a non-null background by default and also, ImageButton.onSetAlpha() method always returns false, scaleType is set to center and it's always inflated as focusable. So if you need that behaviour you can add it.
Another way would be you to create your custom ImageButton that can handle FFImageLoading image loading. For that you can inherit from ImageViewAsync and add the behaviours explained in the paragraph before. So that you can use the FFImageLoading API directly as this custom controls inherits ImageViewAsync.
Instead of that you can also add your custom loading logic as explained here inheriting from ImageLoaderTask and call it like ImageService.Instance.LoadImage(customImageTask)
Finally another way (but hackish and non-performant) would be to create an ImageViewAsync just to hold the result of the FFImageLoading and on the Success callback set the Drawable in the ImageButton:
var myImageButton = myView.FindViewById<ImageButton>(Resource.Id.my_image_button);
var myImageView = new ImageViewAsync(this); // this is the context
ImageService.Instance.LoadUrl(this.Source[position].LogoImagePath)
.Success(() => myImageButton.SetImageDrawable(myImageView.Drawable))
.Into(myImageView);
HIH and if you need any help let me know

Two Way Binding in Xamarin.Forms custom control

I created a custom control in Xamarin and it has a 2 way Bindable property.
public static BindableProperty SelectedItemsProperty =
BindableProperty.Create<MultiSelectList, IEnumerable>(o => o.SelectedItems, default(IEnumerable), BindingMode.TwoWay,
propertyChanged: OnSelectedItemsChanged);
public IEnumerable SelectedItems
{
get { return (IEnumerable)GetValue(SelectedItemsProperty); }
set { SetValue(SelectedItemsProperty, value); }
}
but the binding works only from ViewModel to the control. if the property value changes in the control it is not getting reflected in the viewmodel property that is bound to this. Just to be sure I assign new Ienumerable object whenever the property value changes inside the custom control.
I am using Xamarin.Forms version 2.0.1.6505
Any help on how to force the binding from control to viewmodel?
You should use an ObservableCollection instead of IEnumerable. The ObservableCollection will refresh the UI when it changed, not IEnumerable.

Update the view although the model hasn't changed

My app updates the view in response to events dispatched by the model. But what if the model hasn't changed, but I still need to update the view. For example, I've closed and reopened a pop-up. The data to be displayed hasn't changed but the pop-up mediator and the view have to be recreated. My current solution is to force initialization in the mediator's onRegister() method like this:
// Inside of PopUpMediator.as
[Inject]
public var popUpModel:IPopUpModel;
[Inject]
public var popUpView:PopUpView;
override public function onRegister()
{
// Force initialization if the model hasn't changed
popUpView.foo = popUpModel.foo;
// Event based initialization
addContextListener(PopUpModelEvent_foo.CHANGE, foo_changeHandler);
}
Injecting models into mediators isn't a good idea, so I'm wondering What is the best way to init the view when its model hasn't changed?
Well,
I supose you have View1 where you have popup button.
View2 is your popus.
so when View1 button is clicked, you dispatch an event from main mediator that goes to popupCommand where you add popup to contextView, or where you remove it.
You can also have one state inside a model, that will say popupVisible and when you change that property you dispatch a event that is listened in the main mediator and that adds or removes the popup. In that case command would alter the model property instead of adding popup directly to contextView.
Third way is to add popup manually inside the view, and since the stage is being listened to by robotlegs, popup will be mediated automatically.
I've decided to add an event called PopUpViewInitEvent. A command will check if the model was updated while the pop-up was closed. If not it will reinitialize the view by dispatching the PopUpViewInitEvent. The event will contain all the data required to initialize the view. This way I won't have to inject models into my mediator.
[Inject]
public var popUpView:PopUpView;
override public function onRegister()
{
// Batch initialization
addContextListener(PopUpViewInitEvent.INIT, batchInit);
// Gradual initialization
addContextListener(PopUpModelEvent_foo.CHANGE, foo_changeHandler);
addContextListener(PopUpModelEvent_bar.CHANGE, bar_changeHandler);
}
protected function batchInit(event:PopUpViewInitEvent)
{
popUpView.foo = event.foo;
popUpView.bar = event.bar;
}

Resources