Assuming I have the following command
public class SignOutCommand : CommandBase, ISignOutCommand
{
private readonly UserModel _userModel;
private readonly IUserService _userService;
public SignOutCommand(IUserService userService)
{
_userService = userService;
_userModel = App.CurrentUser;
}
public bool CanExecute(object parameter)
{
var vm = parameter as EditProfileViewModel;
return vm != null;
}
public Task<bool> CanExecuteAsync(object parameter)
{
return Task.Run(() => CanExecute(parameter);
}
public void Execute(object parameter)
{
var vm = (EditProfileViewModel)parameter;
var signOutSucceeded = SignOutUser();
if (signOutSucceeded)
{
vm.AfterSuccessfulSignOut();
}
}
public Task ExecuteAsync(object parameter)
{
return Task.Run(() => Execute(parameter);
}
private bool SignOutUser()
{
// populate this so that we get the attached collections.
var user = _userService.GetByEmailAddress(_userModel.Email);
_userService.RevokeLoggedInUser(user);
return true;
}
}
And my XAML has a button
<Button Text="Sign out"
Command="{Binding SignOutCommand}"
CommandParameter="{Binding}" />
What would it take for this to execute the ExecuteAsync instead of Execute? Sorry if this is trivial, I'm quite new to XAML.
Also, I'm actually doing this in Xamarin.Forms XAML, not sure if it makes a difference here.
You could simply implement your command this way
public class SignOutCommand : ICommand
{
public bool CanExecute(object parameter)
{
var vm = parameter as EditProfileViewModel;
return vm != null;
}
public async void Execute(object parameter)
{
Task.Run(() =>
{
var vm = (EditProfileViewModel)parameter;
var signOutSucceeded = SignOutUser();
if (signOutSucceeded)
{
vm.AfterSuccessfulSignOut();
}
}
}
...
}
But then the bound button is not disabled during execution and the command can be executed even if it is already running...
If you need that the command cannot be executed during execution have a look at:
https://mytoolkit.codeplex.com/wikipage?title=AsyncRelayCommand
Source code: https://xp-dev.com/svn/mytoolkit/MyToolkit/Command/
Implementing the CanExecute asynchronously would be more tricky...
Related
I am coming up to speed on Xamarin. I am using "Mastering Xamarin.Forms: App architecture techniques for building multi-platform, native mobile apps with Xamarin.Forms 4, 3rd Edition" as a guide. This had me create a custom navigation service.
Here is the implementation (I skipped the interface for brevity)
namespace wfw_dispenser.Services
{
public class XamarinFormsNavService : INavService
{
readonly IDictionary<Type, Type> _map = new Dictionary<Type, Type>();
public event PropertyChangedEventHandler CanGoBackChanged;
public INavigation XamarinFormsNav { get; set; }
public bool CanGoBack => XamarinFormsNav.NavigationStack?.Any() == true;
public async Task GoBack()
{
if (CanGoBack)
{
await XamarinFormsNav.PopAsync(true);
OnCanGoBackChanged();
}
}
public async Task NavigateTo<TVM>()
where TVM : BaseViewModel
{
await NavigateToView(typeof(TVM));
if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel)
{
((BaseViewModel)XamarinFormsNav.NavigationStack.Last().BindingContext).Init();
}
}
public async Task NavigateTo<TVM, TParameter>(TParameter parameter)
where TVM : BaseViewModel
{
await NavigateToView(typeof(TVM));
if (XamarinFormsNav.NavigationStack.Last().BindingContext is BaseViewModel<TParameter>)
{
((BaseViewModel<TParameter>)XamarinFormsNav.NavigationStack.Last().BindingContext).Init(parameter);
}
}
public void RemoveLastView()
{
if (XamarinFormsNav.NavigationStack.Count< 2)
{
return;
}
var lastView = XamarinFormsNav.NavigationStack[XamarinFormsNav.NavigationStack.Count - 2];
XamarinFormsNav.RemovePage(lastView);
}
public void ClearBackStack()
{
if (XamarinFormsNav.NavigationStack.Count < 2)
{
return;
}
for (var i = 0; i < XamarinFormsNav.NavigationStack.Count - 1; i++)
{
XamarinFormsNav.RemovePage(XamarinFormsNav.NavigationStack[i]);
}
}
public void NavigateToUri(Uri uri)
{
if (uri == null)
{
throw new ArgumentException("Invalid URI");
}
Device.OpenUri(uri);
}
async Task NavigateToView(Type viewModelType)
{
if (!_map.TryGetValue(viewModelType, out Type viewType))
{
throw new ArgumentException("No view found in view mapping for " + viewModelType.FullName + ".");
}
// Use reflection to get the View's constructor and create an instance of the View
var constructor = viewType.GetTypeInfo()
.DeclaredConstructors
.FirstOrDefault(dc => !dc.GetParameters().Any());
var view = constructor.Invoke(null) as Page;
var vm = ((App)Application.Current).Kernel.GetService(viewModelType);
view.BindingContext = vm;
await XamarinFormsNav.PushAsync(view, true);
}
public void RegisterViewMapping(Type viewModel, Type view)
{
_map.Add(viewModel, view);
}
void OnCanGoBackChanged() => CanGoBackChanged?.Invoke(this, new PropertyChangedEventArgs("CanGoBack"));
}
}
It appears to me that there is a NavigateTo that takes a parameter. I tried it and it kind of goes nowhere without any errors in the log. There's nothing in the text about this method to explain how to use it.
I probably have to do something in the "catching" view model for this. Can someone help me out?
First, you must extend from the parameterized version of BaseViewModel. In your case, since you are passing in a PaymentRequest, this would be:
public class CheckoutViewModel : BaseViewModel<PaymentRequest>
Then BaseViewModel<T> has a virtual Init method that you can implement
public class BaseViewModel<TParameter> : BaseViewModel
{
protected BaseViewModel(INavService navService, IAnalyticsService analyticsService)
: base(navService, analyticsService)
{
}
public override void Init()
{
Init(default(TParameter));
}
public virtual void Init(TParameter parameter)
{
}
}
I have two viewmodels. The first SxCaseDetailViewModel contains the details for one SxCaseId. The second viewmodel CaseStaffJoinViewModel containes all of the staff related to the SxCaseId. I am not getting any related data in my CaseStaffJoinViewModel. How do I pass the int SxCaseId to the CaseStaffJoinViewModel so that it will show all related data?
ViewModel SxCaseDetailViewModel
public class SxCaseDetailViewModel : ViewModelBase
{
private ISxCaseDataService _sxCaseDataService;
private ICaseStaffJoinDataService _caseStaffJoinDataService;
private SxCase _selectedSxCase;
public SxCaseDetailViewModel(IConnectionService connectionService,
INavigationService navigationService, IDialogService dialogService,
ICaseStaffJoinDataService caseStaffJoinDataService,
ISxCaseDataService sxCaseDataService)
: base(connectionService, navigationService, dialogService)
{
_sxCaseDataService = sxCaseDataService;
_caseStaffJoinDataService = caseStaffJoinDataService;
}
public SxCase SelectedSxCase
{
get => _selectedSxCase;
set
{
_selectedSxCase = value;
OnPropertyChanged();
}
}
public override async Task InitializeAsync(object navigationData)
{
IsBusy = true;
SelectedSxCase = (SxCase) navigationData;
IsBusy = false;
}
public ICommand CaseStaffJoinTappedCommand => new Command<SxCase>(OnCaseStaffJoinTapped);
private void OnCaseStaffJoinTapped(SxCase selectedSxCase)
{
_navigationService.NavigateToAsync<CaseStaffJoinViewModel>(selectedSxCase);
}
}
ViewModel CaseStaffJoinViewModel
public class CaseStaffJoinViewModel : ViewModelBase
{
private ICaseStaffJoinDataService _caseStaffJoinDataService;
private ObservableCollection<CaseStaffJoin> _caseStaffJoins;
public CaseStaffJoinViewModel(IConnectionService connectionService,
INavigationService navigationService, IDialogService dialogService,
ICaseStaffJoinDataService caseStaffJoinDataService)
: base(connectionService, navigationService, dialogService)
{
_caseStaffJoinDataService = caseStaffJoinDataService;
}
public ObservableCollection<CaseStaffJoin> CaseStaffJoins
{
get => _caseStaffJoins;
set
{
_caseStaffJoins = value;
OnPropertyChanged();
}
}
public override async Task InitializeAsync(object navigationData)
{
if (navigationData is int)
{
IsBusy = true;
// Get caseStaffJoins by id
CaseStaffJoins = (ObservableCollection<CaseStaffJoin>)
await _caseStaffJoinDataService.GetCaseStaffJoinsAsync((int) navigationData);
IsBusy = false;
}
else
{
}
}
}
Data Service
class CaseStaffJoinDataService :BaseService, ICaseStaffJoinDataService
{
HttpClient _client;
private readonly IGenericRepository _genericRepository;
public CaseStaffJoinDataService(IGenericRepository genericRepository,
IBlobCache cache = null) : base(cache)
{
_genericRepository = genericRepository;
_client = new HttpClient();
}
public async Task<ObservableCollection<CaseStaffJoin>> FilterAsync(int sxCaseId)
{
UriBuilder builder = new UriBuilder(ApiConstants.BaseApiUrl)
{
Path = $"{ApiConstants.CaseStaffsEndpoint}/{sxCaseId}"
};
IEnumerable<CaseStaffJoin> caseStaffJoins =
await _genericRepository.GetAsync<IEnumerable<CaseStaffJoin>>(builder.ToString());
return caseStaffJoins?.ToObservableCollection();
}
public async Task<CaseStaffJoin> GetCaseStaffJoinBySxCaseIdAsync(int sxCaseId)
{
UriBuilder builder = new UriBuilder(ApiConstants.BaseApiUrl)
{
Path = $"{ApiConstants.CaseStaffsEndpoint}/{sxCaseId}"
};
var caseStaffJoin = await _genericRepository.GetAsync<CaseStaffJoin>(builder.ToString());
return caseStaffJoin;
}
public async Task<IEnumerable<CaseStaffJoin>> GetCaseStaffJoinsAsync(int sxCaseId)
{
UriBuilder builder = new UriBuilder(ApiConstants.BaseApiUrl)
{
Path = $"{ApiConstants.CaseStaffsEndpoint}/{sxCaseId}"
};
var caseStaffJoins = await _genericRepository.GetAsync<List<CaseStaffJoin>>(builder.ToString());
return caseStaffJoins;
}
you are passing an SxCase in your navigation
private void OnCaseStaffJoinTapped(SxCase selectedSxCase)
{
_navigationService.NavigateToAsync<CaseStaffJoinViewModel>(selectedSxCase);
}
but you are only checking for an int when receiving the data in InitializeAsync
public override async Task InitializeAsync(object navigationData)
{
if (navigationData is int)
the types need to match - you either need to pass an int or check for an SxCase
I am creating an application in Xamarin.Forms - it is an event management application - in the application the user can sign up to events and create events. When you create an event - you are given a code for that event. There is a window in which you can input the code and then it registers you to that event. So for example if someone wants to check who is coming to a birthday party they could put the code on the invitation and the people who receive the invitation could open the app - input the code - and say whether they are going or not and add some messages.
I followed this tutorial to implement SQLite into Xamarin.Forms:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/data-cloud/data/databases
Here is the code:
public class LocalEventDatabase : ILocalEventDatabase
{
static readonly Lazy<SQLiteAsyncConnection> lazyInitializer = new Lazy<SQLiteAsyncConnection>(() =>
{
return new SQLiteAsyncConnection(Constants.DatabasePath, Constants.Flags);
});
static SQLiteAsyncConnection Database => lazyInitializer.Value;
static bool initialized = false;
public LocalEventDatabase()
{
InitializeAsync().SafeFireAndForget(false, new Action<Exception>(async(Exception ex) =>
{
await Acr.UserDialogs.UserDialogs.Instance.AlertAsync(string.Format($"Don't panic! An exception has occurred: {ex.Message}"), "An exception has occurred", "OK");
}));
}
async Task InitializeAsync()
{
if (!initialized)
{
if (!Database.TableMappings.Any(m => m.MappedType.Name == typeof(Event).Name))
{
await Database.CreateTablesAsync(CreateFlags.None, typeof(Event)).ConfigureAwait(false); // not entirely understood what configure await is?
}
initialized = true;
}
}
public async Task<List<Event>> GetRegisteredEventsAsync()
{
return await Database.Table<Event>().ToListAsync();
}
public async Task<Event> GetEventAsync(Guid id)
{
return await Database.Table<Event>().Where(i => i.EventGuid == id).FirstOrDefaultAsync();
}
public async Task<int> InsertEventAsync(Event #event)
{
return await Database.InsertAsync(#event);
}
public async Task<int> DeleteEventAsync(Event #event)
{
return await Database.DeleteAsync(#event);
}
}
Here is the code behind for searching through the codes:
public class FindEventViewModel : ViewModelBase
{
private ObservableCollection<Event> _results;
private ObservableCollection<Event> _events;
public ICommand SearchCommand => new Command(OnSearchCommand);
public string EntryText { get; set; }
// FAB stands = 'floating action button'
public ObservableCollection<Event> Results
{
get => _results;
set
{
_results = value;
RaisePropertyChanged(nameof(Results));
}
}
public ObservableCollection<Event> Events
{
get => _events;
set
{
_events = value;
RaisePropertyChanged(nameof(Events));
}
}
public FindEventViewModel(IDialogService dialogService,
IEventService eventService,
IShellNavigationService shellNavigationService,
IThemeService themeService,
ILocalEventDatabase localEventDatabase) : base(dialogService, eventService, shellNavigationService, themeService, localEventDatabase)
{
}
public async Task<FindEventViewModel> OnAppearing()
{
await LoadData();
return this;
}
private async Task LoadData()
{
var data = await _eventService.GetAllEventsAsync();
Events = new ObservableCollection<Event>(data);
Results = new ObservableCollection<Event>();
}
public async void OnSearchCommand()
{
if (SearchEvents(EntryText).SearchResult == SearchResult.Match)
{
_dialogService.ShowAdvancedToast(string.Format($"Found matching event"), Color.Green, Color.White, TimeSpan.FromSeconds(2));
var eventFound = SearchEvents(EntryText).EventFound;
Results.Add(eventFound);
RaisePropertyChanged(nameof(eventFound));
// may be unneccessary to use it directly rather than with DI but in this case it is needed in order to evaluate what the user has pressed
var result = await UserDialogs.Instance.ConfirmAsync(new Acr.UserDialogs.ConfirmConfig()
{
Title = "Sign up?",
Message = string.Format($"Would you like to sign up to {eventFound.EventTitle}?"),
OkText = "Yes",
CancelText = "No",
});
if (result)
{
await _localEventDatabase.InsertEventAsync(eventFound);
_dialogService.ShowAdvancedToast(string.Format($"You signed up to {eventFound.EventTitle} successfully"), Color.CornflowerBlue, Color.White, TimeSpan.FromSeconds(2));
MessagingCenter.Send(eventFound as Event, MessagingCenterMessages.EVENT_SIGNEDUP);
}
}
else
{
_dialogService.ShowAdvancedToast(string.Format($"No event found matching query - retry?"), Color.Red, Color.White, TimeSpan.FromSeconds(2));
}
}
public EventSearchResult SearchEvents(string code)
{
foreach (Event _event in Events)
{
if (_event.EventCode == code)
{
return new EventSearchResult()
{
SearchResult = SearchResult.Match,
EventFound = _event,
};
}
}
return new EventSearchResult()
{
SearchResult = SearchResult.NoMatch,
};
}
}
The method returns a search result and the event found. One problem - the user can sign up to the same event twice. In the tutorial I followed it doesn't mention how to check for any duplicate items in the database table?
I'm currently working on a Xamarin.iOS project that uses a web-api to gather data. However, I'm running into some problems trying to pass the user input from a textfield to the Tableview that gets the result from the api.
To do this I've followed the example on the MvvmCross documentation.
The problem is that the input from the Textfield never reaches the 'Filter' property in my TableviewController's viewmodel. I think I'm not passing the string object correctly to my IMvxNavigationService when called.
To clarify, in my UserinputViewController I'm binding the textfield's text like so:
[MvxFromStoryboard(StoryboardName = "Main")]
public partial class SearchEventView : MvxViewController
{
public SearchEventView (IntPtr handle) : base (handle)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
MvxFluentBindingDescriptionSet<SearchEventView, SearchEventViewModel> set = new MvxFluentBindingDescriptionSet<SearchEventView, SearchEventViewModel>(this);
set.Bind(btnSearch).To(vm => vm.SearchEventCommand);
set.Bind(txtSearchFilter).For(s => s.Text).To(vm => vm.SearchFilter);
set.Apply();
}
}
The Viewmodel linked to this ViewController looks like this:
public class SearchEventViewModel : MvxViewModel
{
private readonly IMvxNavigationService _navigationService;
private string _searchFilter;
public string SearchFilter
{
get { return _searchFilter; }
set { _searchFilter = value; RaisePropertyChanged(() => SearchFilter); }
}
public SearchEventViewModel(IMvxNavigationService mvxNavigationService)
{
this._navigationService = mvxNavigationService;
}
public IMvxCommand SearchEventCommand {
get {
return new MvxCommand<string>(SearchEvent);
}
}
private async void SearchEvent(string filter)
{
await _navigationService.Navigate<EventListViewModel, string>(filter);
}
}
And finally, TableviewController's viewmodel looks like this:
public class EventListViewModel : MvxViewModel<string>
{
private readonly ITicketMasterService _ticketMasterService;
private readonly IMvxNavigationService _navigationService;
private List<Event> _events;
public List<Event> Events
{
get { return _events; }
set { _events = value; RaisePropertyChanged(() => Events); }
}
private string _filter;
public string Filter
{
get { return _filter; }
set { _filter = value; RaisePropertyChanged(() => Filter); }
}
public EventListViewModel(ITicketMasterService ticketMasterService, IMvxNavigationService mvxNavigationService)
{
this._ticketMasterService = ticketMasterService;
this._navigationService = mvxNavigationService;
}
public IMvxCommand EventDetailCommand {
get {
return new MvxCommand<Event>(EventDetail);
}
}
private void EventDetail(Event detailEvent)
{
_navigationService.Navigate<EventDetailViewModel, Event>(detailEvent);
}
public override void Prepare(string parameter)
{
this.Filter = parameter;
}
public override async Task Initialize()
{
await base.Initialize();
//Do heavy work and data loading here
this.Events = await _ticketMasterService.GetEvents(Filter);
}
}
Whenever trying to run, the string object 'parameter' in my TableviewController's Prepare function remains 'null' and I have no idea how to fix it. Any help is greatly appreciated!
I believe the issue is with your command setup
new MvxCommand<string>(SearchEvent);
As this command is being bound to a standard UIButton. It will not pass through a parameter value of your filter but null instead. So the string parameter generic can be removed. Additionally, as you want to execute an asynchronous method I would suggest rather using MvxAsyncCommand
new MvxAsyncCommand(SearchEvent);
Then in terms of SearchEvent method you can remove the parameter. The value of filter is bound to your SearchFilter property. It is this property's value that you want to send as the navigation parameter.
private async Task SearchEvent()
{
await _navigationService.Navigate<EventListViewModel, string>(SearchFilter);
}
I'm developing an asynchronous application using WPF and MVVM, but I can't seem to get an async method to run inside my relaycommand.
I have a button on my WPF view hooked up to a relaycommand in my viewmodel, which trys to call an async method in my model to return a list of results:
/// <summary>
/// Search results
/// </summary>
private ObservableCollection<string> _searchResults = new ObservableCollection<string>();
public IList<string> SearchResults
{
get { return _searchResults; }
}
/// <summary>
/// Search button command
/// </summary>
private ICommand _searchCommand;
public ICommand SearchCommand
{
get
{
_searchCommand = new RelayCommand(
async() =>
{
SearchResults.Clear();
var results = await DockFindModel.SearchAsync(_selectedSearchableLayer, _searchString);
foreach (var item in results)
{
SearchResults.Add(item);
}
//notify results have changed
NotifyPropertyChanged(() => SearchResults);
},
() => bAppRunning); //command will only execute if app is running
return _searchCommand;
}
}
However I get the following exception when the relaycommand tries to execute:
An unhandled exception of type 'System.AggregateException' occurred in mscorlib.dll
Additional information: A Task's exception(s) were not observed either
by Waiting on the Task or accessing its Exception property. As a
result, the unobserved exception was rethrown by the finalizer thread.
I've tried a number of things in this thread to try and resolve the issue with no luck. Does anyone know how to resolve this?
Not sure where your RelayCommand is coming from (MVVM framework or custom implementation) but consider using an async version.
public class AsyncRelayCommand : ICommand
{
private readonly Func<object, Task> execute;
private readonly Func<object, bool> canExecute;
private long isExecuting;
public AsyncRelayCommand(Func<object, Task> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute ?? (o => true);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void RaiseCanExecuteChanged()
{
CommandManager.InvalidateRequerySuggested();
}
public bool CanExecute(object parameter)
{
if (Interlocked.Read(ref isExecuting) != 0)
return false;
return canExecute(parameter);
}
public async void Execute(object parameter)
{
Interlocked.Exchange(ref isExecuting, 1);
RaiseCanExecuteChanged();
try
{
await execute(parameter);
}
finally
{
Interlocked.Exchange(ref isExecuting, 0);
RaiseCanExecuteChanged();
}
}
}
Ok so I actually managed to resolve the issue by making a change to the way I run my async method.
I changed this:
var results = await DockFindModel.SearchAsync();
To this:
var results = await QueuedTask.Run(() => DockFindModel.SearchAsync());
Although i'm a bit confused as to why I need to await Task.Run() when relaycommand is already accepting async lambda. I'm sure it'll become clear with time however.
Thanks to those who commented.
That RelayCommand class is probably accepting a parameter of type Action. Since you are passing an async lambda, we are having an async void scenario here.
Your code will be fired and forgot. Consider using an ICommand implementation that takes Func<Task> as a parameter instead of Action.