Can I call MVVM cross command method from Ios based on condition? - xamarin

I have below MVVM cross command in my viewmodel. I want to call this based on condition from iOS. Is this possible?
Command
public IMvxCommand LoginCommand
{
get
{
return _loginCommand ?? (_loginCommand = new MvxCommand(async () => await ExecLoginClick()));
}
}
iOS Binding
var bindings = this.CreateBindingSet<LoginView, LoginViewModel>();
bindings.Bind(username).To(vm => vm.Email);
bindings.Bind(password).To(vm => vm.Password);
bindings.Bind(login_button).To(vm => vm.LoginCommand);
bindings.Bind(forgot_button).To(vm => vm.ForgotCommand);
bindings.Bind(register_button).To(vm => vm.GetSignUpCommand);
//bindings.Bind(btn_facebook).To(vm=>vm.)
bindings.Apply();

You can use CanExecute for this.
public IMvxCommand LoginCommand
{
get
{
return _loginCommand ??
(_loginCommand = new MvxAsyncCommand(ExecLoginClick, CanLogin));
}
}
private bool CanLogin()
{
if ( /*your condition*/)
{
return true;
}
return false;
}
private Task ExecLoginClick()
{
//...
}
And in every method, that affects your condition. You have to call
LoginCommand.RaiseCanExecuteChanged();
The Button is disabled or enabled based on the return value of CanExecute.
If you want to execute your command from your view, you should inherit from the generic MvxViewController<T> or MvxActivity<T> like.
public class LoginView : MvxViewController<LoginViewViewModel>
// or
public class LoginView : MvxActivity<LoginViewViewModel>
And then you can call
if(/*condition*/)
{
ViewModel.LoginCommand.Execute();
}

Related

How to prevent firing of event multiple times on rapid click of a button in Xamarin forms

How can I avoid invoking of the same event multiple times when a button is clicked rapidly.
Below is the code:
I've created a Custom Delegate Command as below
View Model
namespace TestProject.ViewModels
{
public class TestViewModel
{
public CustomDelegateCommand MenuButtonClickCommand { get; set; }
public TestViewModel()
{
MenuButtonClickCommand = new CustomDelegateCommand (async () => await ShowMenuAction());
}
private async Task ShowMenuAction()
{
//await some stuff
}
}
}
CustomDelegateCommand.cs
public class CustomDelegateTimerCommand : DelegateCommand
{
public CustomDelegateTimerCommand(Action executeMethod, Func<bool> validateMethod, Action onBusy = default(Action)) : base(executeMethod)
{
BackgroundTaskWaitHandle = new EventWaitHandle(true, EventResetMode.ManualReset);
_validateMethod = validateMethod;
_onBusy = onBusy;
}
}
The problem I'm facing is whenever a user clicks on the button rapidly, the menu list popup is opening multiple times.
I have lot of commands in my project and I need a solution that would work globally.
I tried to resolve the issue like below using ObservesCanExecute() but I don't like the idea of creating a separate variable for every command as I've a lot of commands in my project and I don't want the button to go in to disabled state when CanExecute = false.
ViewModel
MenuButtonClickCommand = new CustomDelegateCommand (async () => await ShowMenuAction().ObservesCanExecute(() => CanExecute );
private async Task ShowMenuAction()
{
CanExecute = false;
//await some stuff
CanExecute = true;
}
Any help is much appreciated!
There are 2 solutions to it. One is when you use, MVVM other is when you dont.
The non MVVM solution is delaying the execution of method for certain amount of time, like this:
public class SingleClickListener
{
private bool hasClicked;
private Action<object, EventArgs> _setOnClick;
public SingleClickListener(Action<object, EventArgs> setOnClick)
{
_setOnClick = setOnClick;
}
public void OnClick(object v, EventArgs e)
{
if (!hasClicked)
{
_setOnClick(v, e);
hasClicked = true;
}
reset();
}
private void reset()
{
Android.OS.Handler mHandler = new Android.OS.Handler();
mHandler.PostDelayed(new Action(() => { hasClicked = false; }), 500);
}
}
And then when you subscribe the onclick event:
var buttonNa = new Button { Text = "Test Button" };
buttonNa.Clicked += new SingleClickListener((sender, e) =>
{
//DO something
}).OnClick;
The Mvvm solution is bit more complicated, but its not as hacky.
TestCommand = new Command(
execute: async () =>
{
IsEditing = true;
RefreshCanExecutes();
//Fire Method
TestMethod();
},
canExecute: () =>
{
return !IsEditing;
});
public void RefreshCanExecutes()
{
(TestCommand as Command).ChangeCanExecute();
}
public void TestMethod()
{
//DO something
IsEditing = false;
RefreshCanExecutes();
}
Obviously dont forget to bind your commands to xaml :)
also second solution actually disables the button, so user cannot even tap it, first one however only ignores further taps, till time delay has finished.
Create a new class which inherits from Xamarin.Forms.Button with delay in click event, than add it to your xmal.
public class DelayedButton : Xamarin.Forms.Button
{
public DelayedButton()
{
this.Clicked += DelayedButton_Clicked;
}
async private void DelayedButton_Clicked(object sender, EventArgs e)
{
this.IsEnabled = false;
await Task.Delay(Delay);
this.IsEnabled = true;
}
public int Delay { get; set; } = 500;
}
In XAML:
<yourNameSpace:DelayedButton Delay="300" Text="DelayedButton" Command="{Binding ClickCommand}"/>

How to pass parameters and set properties while navigating to a new view model (initializeAsync)

I am using the design pattern mvvm, I have a view model locator and a view model base class.
The view model locator finds what view is associated to the view model. I also wrote a navigation service and one of my methods (NavigateTo) takes in a parameter (an object). the method is to navigate to a view model associated with the view.
namespace StudentData
{
public class StudentOverViewViewModel : ViewModelBase
{
private DataSet studentData;
public Icommand getDetails { get; set; }
public DataSet _data
{
get { return studentData; }
set
{
studentData = value;
RaisePropertyChanged(() => studentData);
}
}
public StudentOverViewViewModel (DataSet studentData)
{
this.studentData = studentData;
getDetails = new Command(Details);
}
public async Task getDetails()
{
// api calls done to retrieve data and set studentData to the current student data
await NavigationService.NavigateToAsync<StudentDetailViewModel>(studentData );
}
}
}
For the second view model I have :
namespace StudentData
{
public class StudentDetailViewModel: ViewModelBase
{
private DataSet Data;
public DataSet _Data
{
get
{
return Data;
}
set
{
Data= value;
RaisePropertyChanged(() => Data);
}
}
}
public StudentDetailViewModel(DataSet Data)
{
this.Data = Data;
}
public override async Task InitializeAsync(object navigationData)
{
if(navigationData is DataSet)
{
Data = (DataSet) navigationData; // after the page is initialized, the variables or properties/ models are not updated and is still null
}
}
}
My issue is that in my initializeAsync method in the second view model, I set the value and property for data, but after the method is done it set all the values back to null.
Thank you in advance for your help.
private async Task InternalNavigateToAsync(Type viewModelType, object
parameter)
{
Page page = CreatePage(viewModelType, parameter);
if (page is UserAuthenticateView)
{
Application.Current.MainPage = new CustomNiavigationView(page);
}
else
{
var navigationPage = Application.Current.MainPage as CustomNiavigationView;
if (navigationPage != null)
{
await navigationPage.PushAsync(page);
}
else
{
Application.Current.MainPage = new CustomNiavigationView(page);
}
}
await (page.BindingContext as ViewModelBase).InitializeAsync(parameter);
}

Disable Wrapping of Controller Results

I am currently using v3.2.5 of Abp.AspNetCore.
I am trying to integrate an Alpha package of Microsoft.AspNetCore.OData into the project which is so far looking ok.
However when i try and query the metadata controller http://localhost:51078/odata/v1/$metadata the result is wrapped.
Now this was an issue for the ODataControllers as well, but i could simply add
the [DontWrapResult] attribute.
I dont have direct access to the MetadataController so i am unable to add the attribute. Is there anyway to disable wrapping for an Abp project?
Thanks
Edit
Here is the current ConfigureServices method
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services
.AddMvc()
.AddJsonOptions(options => { options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; });
services
.AddAuthentication()
.AddCsDeviceAuth(options => { });
services
.AddOData();
//Configure Abp and Dependency Injection
var provider = services.AddAbp<PortalWebODataModule>(options =>
{
//Configure Log4Net logging
options.IocManager.IocContainer.AddFacility<LoggingFacility>(
f => f.LogUsing<Log4NetLoggerFactory>().WithConfig("log4net.config")
);
});
services.Configure<MvcOptions>(options =>
{
var abpResultFilter = options.Filters.First(f => f is AbpResultFilter);
options.Filters.Remove(abpResultFilter);
options.Filters.AddService(typeof(ODataResultFilter));
});
return provider;
}
You can implement IResultFilter and set WrapOnSuccess to false:
public class ResultFilter : IResultFilter, ITransientDependency
{
private readonly IAbpAspNetCoreConfiguration _configuration;
public ResultFilter(IAbpAspNetCoreConfiguration configuration)
{
_configuration = configuration;
}
public void OnResultExecuting(ResultExecutingContext context)
{
if (context.HttpContext.Request.Path.Value.Contains("odata"))
{
var methodInfo = context.ActionDescriptor.GetMethodInfo();
var wrapResultAttribute =
GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(
methodInfo,
_configuration.DefaultWrapResultAttribute
);
wrapResultAttribute.WrapOnSuccess = false;
}
}
public void OnResultExecuted(ResultExecutedContext context)
{
// No action
}
private TAttribute GetSingleAttributeOfMemberOrDeclaringTypeOrDefault<TAttribute>(MemberInfo memberInfo, TAttribute defaultValue = default(TAttribute), bool inherit = true)
where TAttribute : class
{
return memberInfo.GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
?? memberInfo.DeclaringType?.GetTypeInfo().GetCustomAttributes(true).OfType<TAttribute>().FirstOrDefault()
?? defaultValue;
}
}
Then, in Startup class, add the filter in ConfigureServices method:
services.AddMvc(options =>
{
options.Filters.AddService(typeof(ResultFilter));
});
References:
AbpResultFilter.OnResultExecuting
ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault
Alternative solution; to completely disable WrapResult behavior within the system ( at the Core module registration):
var abpAspNetCoreConfiguration = Configuration.Modules.AbpAspNetCore();
abpAspNetCoreConfiguration.DefaultWrapResultAttribute.WrapOnSuccess = false;
abpAspNetCoreConfiguration.DefaultWrapResultAttribute.WrapOnError = false;
abpAspNetCoreConfiguration
.CreateControllersForAppServices(
typeof(AccessApplicationModule).GetAssembly()
);
WrapOnSuccess and WrapOnError flags can be set to false values.
ABP v6.5 and later
Implement IWrapResultFilter and add it to WrapResultFilters in the module's PreInitialize method.
See https://stackoverflow.com/questions/70947461/how-to-control-response-wrapping-in-abp-on-a-per-route-basis/70955045#70955045 for more details.
Before ABP v6.5
...including ABP v3.2.5 mentioned in the question.
Subclass AbpResultFilter:
using Abp.AspNetCore.Configuration;
using Abp.AspNetCore.Mvc.Results;
using Abp.AspNetCore.Mvc.Results.Wrapping;
using Microsoft.AspNetCore.Mvc.Filters;
using System;
namespace AbpODataDemo.Web.Host.Filters
{
public class ODataResultFilter : AbpResultFilter
{
public ODataResultFilter(IAbpAspNetCoreConfiguration configuration, IAbpActionResultWrapperFactory actionResultWrapperFactory)
: base(configuration, actionResultWrapperFactory)
{
}
public override void OnResultExecuting(ResultExecutingContext context)
{
if (context.HttpContext.Request.Path.Value.StartsWith("/odata", StringComparison.InvariantCultureIgnoreCase))
{
return;
}
base.OnResultExecuting(context);
}
}
}
Replace AbpResultFilter with it in the Startup ConfigureServices method:
services.PostConfigure<MvcOptions>(options =>
{
var index = options.Filters.IndexOf(new ServiceFilterAttribute(typeof(AbpResultFilter)));
if (index != -1)
{
options.Filters.RemoveAt(index);
options.Filters.Insert(index, new ServiceFilterAttribute(typeof(ODataResultFilter)));
}
});
Reference: https://github.com/aspnetboilerplate/sample-odata/pull/16

MvvmCross - how to bind to SelectedItem in iOS TableView?

I have a TableView in iOS and, in my ViewModel, I have a property to Selected Item in TableView, but I don't know how to bind the Selected Item for this property. How can I do that? My project is cross-platform. I have an Android project and an iOS project. In Android project, I did the bind:
<Mvx.MvxListView
android:id="#+id/lstViewTasks"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:clickable="true"
android:focusableInTouchMode="true"
android:choiceMode="multipleChoice"
local:MvxBind="ItemsSource Tasks; SelectedItem SelectedTask; ItemClick ShowTaskCommand"
local:MvxItemTemplate="#layout/projectmytasksitem" />
but I can't do a equivalent bind in iOS.
That's my TableViewController:
[Register("ProjectMyTasksViewc")]
public class ProjectMyTasksViews : MvxTableViewController<ProjectMyTasksViewModel>
{
//other things
var source = new MvxSimpleTableViewSource(TableView, ProjectMyTasksItem.Key, ProjectMyTasksItem.Key);
TableView.Source = source;
this.CreateBinding(source).To<ProjectMyTasksViewModel>(viewModel => viewModel.Tasks).Apply();
this.CreateBinding(source).For(s => s.SelectedItem).To<ProjectMyTasksViewModel>(viewModel => viewModel.SelectedTask).Apply();
this.CreateBinding(source).For(tableSource => tableSource.SelectionChangedCommand).To<ProjectMyTasksViewModel>(viewModel => viewModel.ShowTaskCommand).Apply();
}
Here is my ViewModel:
public class ProjectMyTasksViewModel : MvxViewModel
{
public Action ShowTaskCommandAction { get; set; }
private IList<Task> _tasks;
public IList<Task> Tasks
{
get { return _tasks; }
set { _tasks = value; RaisePropertyChanged(() => Tasks); }
}
private Task _selectedTask;
public Task SelectedTask
{
get { return _selectedTask; }
set { _selectedTask = value; RaisePropertyChanged(() => SelectedTask); }
}
private MvxCommand _showTaskCommand;
public MvxCommand ShowTaskCommand
{
get
{
_showTaskCommand = _showTaskCommand ?? (_showTaskCommand = new MvxCommand(ExecuteShowTaskCommand));
return _showTaskCommand;
}
}
private void ExecuteShowTaskCommand()
{
if (!SelectedTask.IsCompleted)
{
ShowTaskCommandAction?.Invoke();
}
}
}
I believe it has to do with the timing of your ShowTaskCommand getting executed vs the set of SelectedTask. So if you commented out the code inside ExecuteShowTaskCommand and place a breakpoint inside ExecuteShowTaskCommand as well as the set of SelectedTask you would find that the ExecuteShowTaskCommand is running first and then the set of the SelectedTask.
Alternative implementation
To avoid the timing issue you can instead pass the selected task into your command as a parameter.
MvxCommand<Task> _showTaskCommand;
public MvxCommand<Task> ShowTaskCommand =>
_showTaskCommand ?? (_showTaskCommand = new MvxCommand<Task>(ExecuteShowTaskCommand));
private void ExecuteShowTaskCommand(Task selectedTask)
{
if (!selectedTask.IsCompleted)
{
ShowTaskCommandAction?.Invoke();
}
}

ReactiveUI: CanExecute with a non collection property

I've seen question ReactiveUI: Using CanExecute with a ReactiveCommand, however my issue is that I have a string property, UniqueID, and I want it to only execute when it has a length equal to 7. I cannot seem to come up with an observer that doesn't crash the program. What is the correct simple way to do this?
public class MainViewModel : ReactiveValidatedObject
{
public MainViewModel()
{
RetrieveRecord = new ReactiveAsyncCommand(/* what goes here for CanExecute */);
RetrieveRecord.Subscriber(x => Record = new Record(UniqueId));
// or do we use the method RetrieveRecord.CanExecute()?
// the next line crashes the app
RetrieveRecord.CanExecute(UniqueId.Length == 7);
}
public ReactiveAsyncCommand RetrieveRecord { get; private set; }
string _uniqueId;
public string UniqueId
{
get { return _uniqueId; }
set
{
_clientId = value;
this.RaisePropertyChanged(x => x.UniqueId);
}
}
}
How about:
RetrieveRecord = new ReactiveAsyncCommand(
this.WhenAny(x => x.UniqueId, x => x.Value.Length == 7));

Resources