Execute ReactiveUI Command from Event in Xamarin Forms - xamarin.forms

I have entry fields in a Xamarin Forms page that I want to trigger a ReactiveUI command when the user is finished entering text into them. I am using ReactiveUI.Events.XamForms and am trying to trigger a command based off of the Unfocused event, but I am not sure how to set up the command to get that to work.
Here is my XAML:
<?xml version="1.0" encoding="utf-8" ?>
<rxui:ReactiveContentPage
x:Class="XamarinReactiveUISwipeView.MainPage"
x:TypeArguments="vm:MainPageViewModel"
xmlns:vm="clr-namespace:XamarinReactiveUITest.ViewModel;assembly=XamarinReactiveUITest"
xmlns:rxui="clr-namespace:ReactiveUI.XamForms;assembly=ReactiveUI.XamForms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:ios="clr- namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
xmlns="http://xamarin.com/schemas/2014/forms"
ios:Page.UseSafeArea="true">
<StackLayout>
<StackLayout Margin="10,0,0,0" Orientation="Horizontal">
<Label Text="Task ID: " />
<Entry x:Name="EntryTaskID" />
</StackLayout>
<StackLayout Margin="10,0,0,0" Orientation="Horizontal">
<Label Text="Task Name: " />
<Entry x:Name="EntryTaskName" />
</StackLayout>
</StackLayout>
</rxui:ReactiveContentPage>
Here is my code behind:
public partial class MainPage : ReactiveContentPage<MainPageViewModel>
{
public MainPage()
{
InitializeComponent();
ViewModel = new MainPageViewModel();
this.WhenActivated(disposable =>
{
this.Bind(ViewModel, vm => vm.TheTaskItem.TaskID, page => page.EntryTaskID.Text)
.DisposeWith(disposable);
this.Bind(ViewModel, vm => vm.TheTaskItem.TaskName, page => page.EntryTaskName.Text)
.DisposeWith(disposable);
EntryTaskName.Events().Unfocused.InvokeCommand(ViewModel, vm => vm.TheCommand);
});
}
}
here is my model:
public class TaskItem
{
public TaskItem() { }
public string TaskID { get; set; }
public string TaskName { get; set; }
}
and here is my view model:
public class MainPageViewModel : ReactiveObject
{
public MainPageViewModel()
{
TheTaskItem = new TaskItem { TaskID = "1", TaskName = "TheTaskName" };
TheCommand = ReactiveCommand.Create<FocusEventArgs, Unit>(ExecuteTheCommand);
}
public ReactiveCommand<FocusEventArgs, Unit> TheCommand { get; }
private void ExecuteTheCommand(FocusEventArgs args)
{
//do something
}
private TaskItem _theTaskItem;
public TaskItem TheTaskItem
{
get => _theTaskItem;
set => this.RaiseAndSetIfChanged(ref _theTaskItem, value);
}
}
In the view model above, it won't compile, but I can't figure out how to set up the ExecuteTheCommand method. The error is:
'void MainPageViewModel.ExecuteTheCommand(FocusEventArgs)' has the wrong return type
But in looking at examples, it looked like methods with void returns use the Unit type.
What do I need to do here to set up the command properly to get this to work?

In comment above, OP says that this works if change:
TheCommand = ReactiveCommand.Create<FocusEventArgs, Unit>(ExecuteTheCommand);
to:
TheCommand = ReactiveCommand.Create<FocusEventArgs>(ExecuteTheCommand);
Unexpectedly, the type of the variable that holds the result of Create, does not use that same generic type signature. It needs to be:
public ReactiveCommand<FocusEventArgs, Unit> TheCommand { get; }
According to the doc in link by Rodney Littles in comment, Unit is used to represent "void return type". (Generic type "TOutput" would be "void"; but "void" is not a valid Generic type.)

Related

How to update label in Xamarin Forms

Beginner here.
I'm trying to get the exact time, when this operation was executed successfully and print it on label. The problem is that when I click the button, the label doesn't update the text.
namespace HGB.Droid.Helpers
{
public class CallServiceHelper : ICallServiceHelper
{
IContactsHelper contactsHelper = DependencyService.Get<IContactsHelper>();
List<Repository> ObjContactList = new List<Repository>();
LabelModel labelModel = new LabelModel();
Context context = Android.App.Application.Context;
HttpClient client = new HttpClient();
public async Task UpdatePhonebook()
{
if (NetworkCheck.IsInternet())
{
var response = await client.GetAsync("http://mmmmmmmmmmm.aspx");
if (response.IsSuccessStatusCode)
{
string contactsJson = await response.Content.ReadAsStringAsync();
var list = JsonConvert.DeserializeObject<List<Repository>>(contactsJson);
contactsHelper.DeleteContact();
ObjContactList = list;
foreach (Repository obj in ObjContactList)
{
contactsHelper.CreateContacts(obj.name, obj.phone);
}
Device.BeginInvokeOnMainThread(() =>
{
labelModel.UpdateLabelValue.Execute(DateTime.Now.ToString());
});
}
}
else
{
Device.BeginInvokeOnMainThread(() =>
{
Toast.MakeText(context, "Error", ToastLength.Long).Show();
});
}
}
I'm calling this function on UI button
public partial class MainPage : ContentPage
{
ICallServiceHelper callServiceHelper = DependencyService.Get<ICallServiceHelper>();
public MainPage()
{
InitializeComponent();
}
private async void updateContactsBtn_Clicked(object sender, EventArgs e)
{
await callServiceHelper.UpdatePhonebook();
}
}
This is my ViewModel
public class LabelModel : BindableObject
{
string dateValue = "Date Value";
public LabelModel()
{
UpdateLabelValue = new Command<string>(UpdateLabel);
}
public ICommand UpdateLabelValue { get; }
public string DateDisplay
{
get => dateValue;
set
{
dateValue = value;
OnPropertyChanged(nameof(DateDisplay));
}
}
void UpdateLabel(string newLabel)
{
DateDisplay = newLabel;
}
}
And this is my Xaml file
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:HGB.ViewModel"
x:Class="HGB.MainPage">
<ContentPage.BindingContext>
<local:LabelModel/>
</ContentPage.BindingContext>
<StackLayout HorizontalOptions="Center" VerticalOptions="Center" Spacing="10">
<Button
Text="Update Phonebook"
x:Name="updateContactsBtn"
Clicked="updateContactsBtn_Clicked"
/>
<Label
VerticalOptions="Center"
HorizontalOptions="Center"
Text="{Binding DateDisplay}"
/>
</StackLayout>
I'm using the Helper method in my Foreground Service class, where it
gets called every 24 hours. What I'm trying to achieve is print the
exact time, when the phonebook was successfully updated and print that
date to my label.
For your problem, a simple method is to use MessagingCenter just as Jason mentioned.
You can send message in your CallServiceHelper and subscribe to this message in your ViewModel(LabelModel.cs).
Please refer to the following code:
1.In the constructor of your ViewMode(LabelModel.cs),subscribe to this message:
public LabelModel()
{
MessagingCenter.Subscribe<object, string>(this, "time", (sender, args) =>
{
System.Diagnostics.Debug.WriteLine("received time is: "+ args);
DateDisplay = args;
});
}
2.In your CallServiceHelper , public your message:
MessagingCenter.Send<object, string>(this, "time", "2022-4-8");

How to bind map/pin in view model properly in Xamarin.Forms?

I followed the doc, trying to bind the pin, but failed. The map is always showing the default position Rome. Here is the source code:
In DetailPage.xmal:
<Frame Margin="10,5"
CornerRadius="10"
Padding="0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="300" />
</Grid.RowDefinitions>
<maps:Map MapType="Street" Grid.Row="0" ItemsSource="{Binding WorkPlace}">
<maps:Map.ItemTemplate>
<DataTemplate>
<maps:Pin Position="{Binding Position}"
Address="{Binding Address}"
Label="{Binding Description}" />
</DataTemplate>
</maps:Map.ItemTemplate>
</maps:Map>
</Grid>
</Frame>
In DetailPageModel.cs:
public class DetailPageModel : PageModelBase
{
private Timesheet _detailedTimesheet;
private ObservableCollection<Location> _workPlace;
public ObservableCollection<Location> WorkPlace
{
get => _workPlace;
set => SetProperty(ref _workPlace, value);
}
public ReportDetailPageModel()
{
}
public override async Task InitializeAsync(object navigationData)
{
if (navigationData is Timesheet selectedTimesheet)
{
_detailedTimesheet = selectedTimesheet;
WorkPlace = new ObservableCollection<Location>()
{
new Location(
_detailedTimesheet.ProjectAddress,
"Test Location",
new Position(_detailedTimesheet.ProjectLatitude, _detailedTimesheet.ProjectLongitude))
};
}
await base.InitializeAsync(navigationData);
}
}
In Location.cs:
public class Location : ExtendedBindableObject
{
Position _position;
public string Address { get; }
public string Description { get; }
public Position Position
{
get => _position;
set => SetProperty(ref _position, value);
}
public Location(string address, string description, Position position)
{
Address = address;
Description = description;
Position = position;
}
}
In ExtendedBindableObject.cs:
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value))
{
return false;
}
storage = value;
OnPropertyChanged(propertyName);
return true;
}
Since the navigationData is correctly received in view model and the page's binding context is also working, I just don't know what could be missing. Any hint would be appreciated!
And actually I have one more confusion, why does the official doc use a custom Location class instead of the Pin class, as Pin inherits from Element/BindableObject/Object?

Xamarin forms - Pass argument to the bindingcontext viewmodel specified in a xaml file

I have a xaml file with with an entry. I am binding the specific entry to a specific viewmodel. But the viewmodel expects a Navigator. How can I pass the navigator from the xaml file to the viewmodel?
<Entry Text="{Binding Signature, Mode=TwoWay}">
<Entry.BindingContext>
<vm:SignaturePopupViewModel>
// I need to pass a navigator..
</vm:SignaturePopupViewModel>
</Entry.BindingContext>
</Entry>
The viewmodel expects a navigation object. I use it to pop the page to go back to the previous page after running some code logic.
public SignaturePopupViewModel(INavigation navigation = null)
{
Navigation = navigation;
SendSignatureCommand = new Command(async () =>
{
await SendSignature();
await Navigation.PopAsync();
});
}
You do not need to use INavigation navigation in your SignaturePopupViewModel in your constructor to achieve the Navigation.
Just use a simple way is
await Application.Current.MainPage.Navigation.PopModalAsync(); Or
await Application.Current.MainPage.Navigation.PopAsync()
like following code.
public class SignaturePopupViewModel
{
public ICommand SendSignatureCommand { protected set; get; }
public SignaturePopupViewModel( )
{
SendSignatureCommand = new Command(async () =>
{
await SendSignature();
// if you use the MainPage = new NavigationPage( new MainPage()); in
//App.xaml.cs use following code.
await Application.Current.MainPage.Navigation.PopAsync();
// if not, just use await Application.Current.MainPage.Navigation.PopModalAsync();
});
}
}
Could you create an instance of the SignaturePopupVM in the ViewModel of that page and then bind the Text to that property instead?
VM:
SignaturePopupViewModel SignaturePopupVMInstance { get; private set; }
public ParentVM()//Constructor
{
SignaturePopupVMInstance = new SignaturePopupViewModel(new Navigator());
}
Xaml:
<Entry Text="{Binding SignaturePopupVMInstance.Signature, Mode=TwoWay}"/>
Edit:
public class TabPageVM{
public ChildVM TheVMForTabOne { get; set; }
public AnotherChildVM TheVMForTabTwo { get; set; }
public TabVM TheVMForTabThree { get; set; }
public TabPageVM(){
TheVMForTabOne = new ChildVM(/*parameters*/);
TheVMForTabTwo = new AnotherChildVM(/*parameters*/);
TheVMForTabThree = new TabVM(/*parameters*/);
}
}
Xaml for tabpage:
<TabbedPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:Views="clr-namespace:App.ViewsForMyTabs"
x:Class="App.TabPageView"
BarBackgroundColor="#EEEEEE"
BarTextColor="Black"
BindingContext="{Binding TheTabbedPageVMInstance}">
<TabbedPage.Children>
<Views:TheViewForTabOne x:Name="TabOneView"
BindingContext="{Binding TheVMForTabOne}"/>
<Views:TheViewForTabTwo x:Name="TabTwoView"
BindingContext="{Binding TheVMforTabTwo}"/>
<Views:TheViewForTabThree x:Name="TabThreeView"
BindingContext="{Binding TheVMforTabThree}"/>
</TabbedPage.Children>
</TabbedPage>
Lets say TheViewForTabOne has the button on it that takes you to the new page. The VM for that view "TheVMForTabOne" would have something like this:
public class ChildVM{
public SignaturePopupViewModel SignaturePopupVMInstance { get; set; }
public Command NavigateToNewPageWithEntry { get; private set; }
public ChildVM(){
SignaturePopupVMInstance = new SignaturePopupViewModel(/*parameters*/);
NavigateToNewPageWithEntry = new Command(() =>{
//Navigate to new page with SignaturePopupVMInstance as the BindingContext
}
}
}
TheViewForTabOne
...
<Label Text="{Binding SignaturePopupVMInstance.Signature}"/>
<Button Command="{Binding NavigateToNewPageWithEntry}"/>
...

Xamarin Forms connect products with user accounts

I'm making a shopping app, it has account for the user, and when the user purchases a product, it should be added to a listview in his account. So I tried to put a static object of the User Class that has a list of Products, and whenever the user clicks the buying button, it should be added to the list. At the same time, the user.xaml is binding to the same object. But it doesn't work. What's the error in my method?
Are there any better ideas to do this?
here's the static field in the App.xaml.cs file
private IDataService _dataService;
public static User TheUser;
public App(IDataService dataService)
{
TheUser = new User();
InitializeComponent();
var unity = new UnityContainer();
unity.RegisterType<IDataService, DataServices>();
ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(unity));
_dataService = dataService;
MainPage = new NavigationPage(new MainPage());
}
and here's the User.xaml.cs property
public User User
{
get { return App.TheUser; }
set
{
if(App.TheUser!= null)
App.TheUser = value;
}
//User class
public class User : Base //Base class implements INotifyPropertyChanged
{
public int Id { get; set; }
public string Name { get; set; }
public ObservableCollection<Product> Products = new ObservableCollection<Product>();
}
public class Base : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Here's the User.Xaml file
<StackLayout>
<AbsoluteLayout>
<Image AbsoluteLayout.LayoutBounds="1 , 0 ,96 ,96" AbsoluteLayout.LayoutFlags="PositionProportional"/>
<Label AbsoluteLayout.LayoutBounds="0 , 50 , 100 , 20" AbsoluteLayout.LayoutFlags="XProportional" Text="First Name"/>
<Label AbsoluteLayout.LayoutBounds="0 , 100 , 100 , 20" AbsoluteLayout.LayoutFlags="XProportional" Text="Last Name"/>
</AbsoluteLayout>
<ListView x:Name="UserListView"
SelectedItem="{Binding SelectedItemCommand}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<StackLayout Spacing="3" FlowDirection="RightToLeft" >
<Image Source="{Binding ProductMainImage}" Aspect="AspectFill" Margin="3" HeightRequest="300" />
<Label Text="{Binding Name ,StringFormat=' الاسم : {0}'}"/>
<Label Text="{Binding Price ,StringFormat=' السعر : {0}'}"/>
<Label Text="{Binding Description ,StringFormat=' الوصف : {0}'}"/>
</StackLayout>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
ok so the property notification doesn't happen automatically. You need to invoke that code to raise the event. This should takes care of the code, but without seeing the xaml, I don't know if the binding is setup correctly.
public User User
{
get { return App.TheUser; }
set
{
if(App.TheUser!= null)
App.TheUser = value;
}
//User class
public class User : Base //Base class implements INotifyPropertyChanged
{
private int _id
public int Id {
get{
return this._id;
}
set{
this._id = value;
OnPropertyChanged("Id");
}
}
private string _name;
public string Name {
get{
return this._name;
}
set{
this._name = value;
OnPropertyChanged("Name");
}
}
private ObservableCollection<Product> _products;
public ObservableCollection<Product> Products
{
get{
return this._products;
}
set{
this._products = value;
OnPropertyChanged("Products");
}
}
}
}
so your listview is not bound to anything...
<ListView x:Name="UserListView"
ItemsSource={Binding Products}
SelectedItem="{Binding SelectedItemCommand}">

Caliburn Action not firing

<ItemsControl DockPanel.Dock="Right" x:Name="Actions">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button x:Name="Action"
HorizontalAlignment="Right"
Content="{Binding Label}"
Margin="3" Width="30"></Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
The above view binds with this viewmodel
public class DeploymentInputViewModel<T> : PropertyChangedBase
{
public BindableCollection<InputActionViewModel> Actions {get;set;}
}
I see my buttons. But when clicking it nothing happen.
The viewModels for InputActionViewModel:
public abstract class InputActionViewModel{
public InputActionViewModel()
{
}
public virtual Task Action()
{
return Task.FromResult<object>(null);
}
public string ActionToolTip { get; set; }
public string Label { get; set; }
public object Value { get; set; }
}
and also
public class InputCertificateActionViewModel : InputActionViewModel
{
[Import]
private IShell _shell;
[Import]
private IWindowsDialogs _dialogs;
private readonly IDeploymentSettingInputViewModel vm;
public InputCertificateActionViewModel(IDeploymentSettingInputViewModel vm)
{
this.vm = vm;
Label = "...";
ActionToolTip = "Pick a Certificate";
}
public bool IsManagementCertificate {get;set;}
public bool IsDeploymentCertificate { get; set; }
public async override Task Action()
{
if(IsManagementCertificate)
{
var subs = await _shell.IdentityModel.GetEnabledSubscriptionsAsync();
foreach(var sub in subs)
{
using (ManagementClient client = CloudContext.Clients.CreateManagementClient(sub.GetCredentials()))
{
var cert = _dialogs.SelectItemDialog("Select a certificate", "Pick one", true,
(await client.ManagementCertificates.ListAsync()).Select(c =>
new SelectItem(c.Thumbprint, Encoding.Default.GetString(c.PublicKey), c, (s) => c.Thumbprint.Contains(s))).ToArray())
.Tag as ManagementCertificateListResponse.SubscriptionCertificate;
this.vm.Value = cert.Thumbprint;
}
}
}else if(IsDeploymentCertificate)
{
}
}
}
I am adding actionViewModels by inserting directly into the observable code at startup.
haveActions.Actions.Add(DI.BuildUp(new InputCertificateActionViewModel(vm)
{
IsDeploymentCertificate = certAttribute.IsDeploymentCertificate,
IsManagementCertificate = certAttribute.IsManagementCertificate,
}));
haveActions is an instance of InputCertificateActionViewModel
Couldn't fit this all in a comment:
I can't have a peek at the Caliburn.Micro at the moment, but it might be something related to calling your method Action.
At a guess though, I'd say that by convention Caliburn.Micro expects to find a method that matches the Action<T> delegate to use for it's Actions, so your public virtual Task Action() won't be located and bound.
Have a quick check by defining a new method with a compatible signature, e.g public void MyMethod() and checking to see if it's located correctly and will function.
If that is the problem, you'll probably want to have a look at the IResult and Coroutines part of the Caliburn.Micro documentation, which looks like it will help you implement your desired behaviour.

Resources