diff --git a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactListPageModel.cs b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactListPageModel.cs index c5687f3..89b5528 100644 --- a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactListPageModel.cs +++ b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactListPageModel.cs @@ -24,6 +24,7 @@ public ContactListPageModel (IDatabaseService databaseService) public override void Init (object initData) { Contacts = new ObservableCollection (_databaseService.GetContacts ()); + AddContact = CoreMethods.CreateCommand(() => CoreMethods.PushPageModel()); } protected override void ViewIsAppearing (object sender, EventArgs e) @@ -52,13 +53,7 @@ public Contact SelectedContact { } } - public Command AddContact { - get { - return new Command (async () => { - await CoreMethods.PushPageModel (); - }); - } - } + public ICommand AddContact { get; private set; } public Command ContactSelected { get { @@ -71,7 +66,7 @@ public Command ContactSelected { public ICommand OpenFirst { get - { + { return new FreshAwaitCommand(async (contact, tcs) => { await CoreMethods.PushPageModel(this.Contacts.First()); diff --git a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactPageModel.cs b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactPageModel.cs index 497f6a2..369e88c 100644 --- a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactPageModel.cs +++ b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/ContactPageModel.cs @@ -2,6 +2,8 @@ using PropertyChanged; using FreshMvvm; using System; +using System.Windows.Input; +using System.Threading.Tasks; namespace FreshMvvmSampleApp { @@ -10,6 +12,12 @@ public class ContactPageModel : FreshBasePageModel { IDatabaseService _dataService; + public ICommand SaveCommand { get; private set; } + public ICommand TestModal { get; private set; } + public ICommand TestModalNavigationBasic { get; private set; } + public ICommand TestModalNavigationTabbed { get; private set; } + public ICommand TestModalNavigationMasterDetail { get; private set; } + public ContactPageModel (IDatabaseService dataService) { _dataService = dataService; @@ -31,62 +39,42 @@ public override void Init (object initData) } else { Contact = new Contact (); } - } - public Command SaveCommand { - get { - return new Command (() => { - _dataService.UpdateContact (Contact); - CoreMethods.PopPageModel (Contact); - } - ); - } + var sharedLock = new SharedLock(); + SaveCommand = CoreMethods.CreateCommand(SaveCommandLogic, sharedLock); + TestModal = CoreMethods.CreateCommand(() => CoreMethods.PushPageModel(null, true), sharedLock); + TestModalNavigationBasic = CoreMethods.CreateCommand(TestModalNavigationBasicLogic, sharedLock); + TestModalNavigationTabbed = CoreMethods.CreateCommand(TestModalNavigationTabbedLogic, sharedLock); + TestModalNavigationMasterDetail = CoreMethods.CreateCommand(TestModalNavigationMasterDetailLogic, sharedLock); } - public Command TestModal { - get { - return new Command (async () => { - await CoreMethods.PushPageModel (null, true); - }); - } + private async Task SaveCommandLogic() + { + _dataService.UpdateContact(Contact); + await CoreMethods.PopPageModel(Contact); } - public Command TestModalNavigationBasic { - get { - return new Command (async () => { - - var page = FreshPageModelResolver.ResolvePageModel (); - var basicNavContainer = new FreshNavigationContainer (page, Guid.NewGuid ().ToString ()); - await CoreMethods.PushNewNavigationServiceModal(basicNavContainer, new FreshBasePageModel[] { page.GetModel() }); - }); - } + private async Task TestModalNavigationBasicLogic() + { + var page = FreshPageModelResolver.ResolvePageModel(); + var basicNavContainer = new FreshNavigationContainer(page, Guid.NewGuid().ToString()); + await CoreMethods.PushNewNavigationServiceModal(basicNavContainer, new FreshBasePageModel[] { page.GetModel() }); } - - public Command TestModalNavigationTabbed { - get { - return new Command (async () => { - - var tabbedNavigation = new FreshTabbedNavigationContainer (Guid.NewGuid ().ToString ()); - tabbedNavigation.AddTab ("Contacts", "contacts.png", null); - tabbedNavigation.AddTab ("Quotes", "document.png", null); - await CoreMethods.PushNewNavigationServiceModal(tabbedNavigation); - }); - } + public async Task TestModalNavigationTabbedLogic() { + var tabbedNavigation = new FreshTabbedNavigationContainer (Guid.NewGuid ().ToString ()); + tabbedNavigation.AddTab ("Contacts", "contacts.png", null); + tabbedNavigation.AddTab ("Quotes", "document.png", null); + await CoreMethods.PushNewNavigationServiceModal(tabbedNavigation); } - public Command TestModalNavigationMasterDetail { - get { - return new Command (async () => { - - var masterDetailNav = new FreshMasterDetailNavigationContainer (Guid.NewGuid ().ToString ()); - masterDetailNav.Init ("Menu", "Menu.png"); - masterDetailNav.AddPage ("Contacts", null); - masterDetailNav.AddPage ("Quotes", null); - await CoreMethods.PushNewNavigationServiceModal(masterDetailNav); - - }); - } + public async Task TestModalNavigationMasterDetailLogic() + { + var masterDetailNav = new FreshMasterDetailNavigationContainer(Guid.NewGuid().ToString()); + masterDetailNav.Init("Menu", "Menu.png"); + masterDetailNav.AddPage("Contacts", null); + masterDetailNav.AddPage("Quotes", null); + await CoreMethods.PushNewNavigationServiceModal(masterDetailNav); } } } diff --git a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/MainMenuPageModel.cs b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/MainMenuPageModel.cs index c83f2b5..d7cad7d 100644 --- a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/MainMenuPageModel.cs +++ b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/MainMenuPageModel.cs @@ -1,28 +1,19 @@ using Xamarin.Forms; using FreshMvvm; +using System.Windows.Input; namespace FreshMvvmSampleApp { public class MainMenuPageModel : FreshBasePageModel { - public MainMenuPageModel () - { - } + public ICommand ShowQuotes { get; private set; } + public ICommand ShowContacts { get; private set; } - public Command ShowQuotes { - get { - return new Command (async () => { - await CoreMethods.PushPageModel (); - }); - } - } - - public Command ShowContacts { - get { - return new Command (async () => { - await CoreMethods.PushPageModel (); - }); - } + public override void Init(object initData) + { + var sharedLock = new SharedLock(); + ShowQuotes = CoreMethods.CreateCommand(() => CoreMethods.PushPageModel(), sharedLock); + ShowContacts = CoreMethods.CreateCommand(() => CoreMethods.PushPageModel(), sharedLock); } } } diff --git a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuoteListPageModel.cs b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuoteListPageModel.cs index b420c9b..c52c537 100644 --- a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuoteListPageModel.cs +++ b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuoteListPageModel.cs @@ -3,6 +3,7 @@ using FreshMvvm; using PropertyChanged; using System.Diagnostics; +using System.Windows.Input; namespace FreshMvvmSampleApp { @@ -21,6 +22,7 @@ public QuoteListPageModel (IDatabaseService databaseService) public override void Init (object initData) { Quotes = new ObservableCollection (_databaseService.GetQuotes ()); + AddQuote = CoreMethods.CreateCommand(() => CoreMethods.PushPageModel()); } protected override void ViewIsAppearing (object sender, System.EventArgs e) @@ -42,13 +44,7 @@ public override void ReverseInit (object value) } } - public Command AddQuote { - get { - return new Command (async () => { - await CoreMethods.PushPageModel (); - }); - } - } + public ICommand AddQuote { get; private set; } Quote _selectedQuote; diff --git a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuotePageModel.cs b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuotePageModel.cs index 814a9e0..32553e1 100644 --- a/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuotePageModel.cs +++ b/samples/FreshMvvmSampleApp/FreshMvvmSampleApp/PageModels/QuotePageModel.cs @@ -1,6 +1,8 @@ using Xamarin.Forms; using PropertyChanged; using FreshMvvm; +using System.Windows.Input; +using System.Threading.Tasks; namespace FreshMvvmSampleApp { @@ -9,6 +11,9 @@ public class QuotePageModel : FreshBasePageModel { IDatabaseService _databaseService; + public ICommand SaveCommand { get; private set; } + public ICommand TestModal { get; private set; } + public Quote Quote { get; set; } public QuotePageModel (IDatabaseService databaseService) @@ -17,27 +22,18 @@ public QuotePageModel (IDatabaseService databaseService) } public override void Init (object initData) - { - Quote = initData as Quote; - if (Quote == null) - Quote = new Quote (); - } + { + Quote = (initData as Quote) ?? new Quote(); - public Command SaveCommand { - get { - return new Command (async () => { - _databaseService.UpdateQuote (Quote); - await CoreMethods.PopPageModel (Quote); - }); - } + var sharedLock = new SharedLock(); + SaveCommand = CoreMethods.CreateCommand(SaveCommandLogic, sharedLock); + TestModal = CoreMethods.CreateCommand(() => CoreMethods.PushPageModel(null, true), sharedLock); } - public Command TestModal { - get { - return new Command (async () => { - await CoreMethods.PushPageModel (null, true); - }); - } + private async Task SaveCommandLogic() + { + _databaseService.UpdateQuote (Quote); + await CoreMethods.PopPageModel (Quote); } } } diff --git a/src/FreshMvvm.Tests/Fixtures/FreshNavigationCommandFixture.cs b/src/FreshMvvm.Tests/Fixtures/FreshNavigationCommandFixture.cs new file mode 100644 index 0000000..65d7199 --- /dev/null +++ b/src/FreshMvvm.Tests/Fixtures/FreshNavigationCommandFixture.cs @@ -0,0 +1,41 @@ +using System; +using NUnit.Framework; +using System.Threading.Tasks; + +namespace FreshMvvm.Tests.Fixtures +{ + [TestFixture] + public class FreshNavigationCommandFixture + { + SharedLock _sharedLock; + + [SetUp] + public void Setup() + { + _sharedLock = new SharedLock(); + } + + [Test] + public async Task OnlyOneCommandWillExecuteTests() + { + //Flow: A executes, B executes, A starts, B ignored, A completes. + int countBefore = 0; + int countAfter = 0; + Func execute = async (obj) => + { + countBefore++; + await Task.Delay(100); + countAfter++; + }; + + //Execute is async void, so this will run in the background. + var first = new FreshNavigationCommand(execute, _sharedLock).ExecuteAsync(null); + var second = new FreshNavigationCommand(execute, _sharedLock).ExecuteAsync(null); + + await Task.WhenAll(first, second); //Need to let execute finish. + + Assert.AreEqual(1, countBefore); + Assert.AreEqual(1, countAfter); + } + } +} diff --git a/src/FreshMvvm.Tests/FreshMvvm.Tests.csproj b/src/FreshMvvm.Tests/FreshMvvm.Tests.csproj index 3a11272..a906621 100644 --- a/src/FreshMvvm.Tests/FreshMvvm.Tests.csproj +++ b/src/FreshMvvm.Tests/FreshMvvm.Tests.csproj @@ -87,6 +87,7 @@ + diff --git a/src/FreshMvvm.Tests/Mocks/MockPageModelCoreMethods.cs b/src/FreshMvvm.Tests/Mocks/MockPageModelCoreMethods.cs index b085d90..dfc7568 100644 --- a/src/FreshMvvm.Tests/Mocks/MockPageModelCoreMethods.cs +++ b/src/FreshMvvm.Tests/Mocks/MockPageModelCoreMethods.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using System.Windows.Input; using Xamarin.Forms; namespace FreshMvvm.Tests.Mocks @@ -209,5 +210,25 @@ public Task SwitchSelectedMaster() where T : FreshBasePag { throw new NotImplementedException(); } + + public ICommand CreateCommand(Func execute, SharedLock sharedLock = null) + { + throw new NotImplementedException(); + } + + public ICommand CreateCommand(Func execute, SharedLock sharedLock = null) + { + throw new NotImplementedException(); + } + + public ICommand CreateCommand(Func execute, SharedLock sharedLock = null) + { + throw new NotImplementedException(); + } + + public ICommand CreateCommand(Func execute, Func stringConverter, SharedLock sharedLock = null) + { + throw new NotImplementedException(); + } } } diff --git a/src/FreshMvvm/FreshMvvm.csproj b/src/FreshMvvm/FreshMvvm.csproj index 5dce036..80cfb94 100644 --- a/src/FreshMvvm/FreshMvvm.csproj +++ b/src/FreshMvvm/FreshMvvm.csproj @@ -44,6 +44,8 @@ + + diff --git a/src/FreshMvvm/FreshNavigationCommand.cs b/src/FreshMvvm/FreshNavigationCommand.cs new file mode 100644 index 0000000..2adb3f3 --- /dev/null +++ b/src/FreshMvvm/FreshNavigationCommand.cs @@ -0,0 +1,136 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace FreshMvvm +{ + /// + /// Command that will execute and ignore all new events until it's finished. + /// Typicaly used to ignore multiple events from doubleclicking by the user. + /// + public class FreshNavigationCommand : IFreshNavigationCommand + { + public event EventHandler CanExecuteChanged + { + add { _sharedLock.SharedCanExecuteChanged += value; } + remove { _sharedLock.SharedCanExecuteChanged -= value; } + } + + private readonly SharedLock _sharedLock; + private readonly Func _execute; + + public FreshNavigationCommand(Func execute, SharedLock sharedLock = null) + { + if (execute == null) + throw new ArgumentException(nameof(execute)); + + _execute = execute; + _sharedLock = sharedLock ?? new SharedLock(); + } + + public FreshNavigationCommand(Func execute, SharedLock sharedLock = null) + : this((obj) => execute(), sharedLock) + { + if (execute == null) + throw new ArgumentException(nameof(execute)); + } + + public bool CanExecute(object parameter) + { + return !_sharedLock.IsLocked; + } + + /// + /// Execute the command with specified parameter. + /// The caller will not be able to await this command. + /// + /// Parameter. +#pragma warning disable RECS0165 // Asynchronous methods should return a Task instead of void + public async void Execute(object parameter) +#pragma warning restore RECS0165 + { + await ExecuteAsync(parameter); + } + + /// + /// Execute the async command with specified parameter. + /// If more than one is executed at the same time, all other than the first one will be ignored. + /// + /// The async task, that can be awaited. + /// Parameter. + public async Task ExecuteAsync(object parameter) + { + if (_sharedLock.TakeLock()) //Ignores code block if lock already taken in SharedLock. + { + try + { + await _execute(parameter); + } + finally + { + _sharedLock.ReleaseLock(); + } + } + } + } + + public class FreshNavigationCommand : FreshNavigationCommand + { + public FreshNavigationCommand(Func execute, SharedLock sharedLock = null) + : base(obj => execute((TValue)obj), sharedLock) + { + if (execute == null) + throw new ArgumentException(nameof(execute)); + } + } + + /// + /// Locking object that can be shared between commands. + /// If multiple commands use the same shared object, only one of them can run simultanious. + /// + public class SharedLock + { + private int _lock; + + public bool IsLocked => _lock != 0; + + public event EventHandler SharedCanExecuteChanged; + + public SharedLock() + { + _lock = 0; + } + + /// + /// Will take a threadsafe lock on the object. + /// + /// true, if lock was taken, false otherwise. + public bool TakeLock() + { + var oldVal = Interlocked.Exchange(ref _lock, 1); //Atomic swap values. + var lockTaken = oldVal == 0; //If the old value was 0, no one else is executing at this time. + + var events = SharedCanExecuteChanged; + if (events != null) + events(this, EventArgs.Empty); + + return lockTaken; + } + + /// + /// Will release the treadsafe lock on the object. + /// + /// true, if lock was released, false otherwise. + public bool ReleaseLock() + { + var oldVal = Interlocked.Exchange(ref _lock, 0); + var lockReleased = oldVal == 1; + + var events = SharedCanExecuteChanged; + if (events != null) + events(this, EventArgs.Empty); + + return lockReleased; + } + } +} \ No newline at end of file diff --git a/src/FreshMvvm/IFreshNavigationCommand.cs b/src/FreshMvvm/IFreshNavigationCommand.cs new file mode 100644 index 0000000..f6d5c68 --- /dev/null +++ b/src/FreshMvvm/IFreshNavigationCommand.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace FreshMvvm +{ + public interface IFreshNavigationCommand : ICommand + { + Task ExecuteAsync(object parameter); + } +} diff --git a/src/FreshMvvm/IPageModelCoreMethods.cs b/src/FreshMvvm/IPageModelCoreMethods.cs index 1b3c0dc..95a02cd 100644 --- a/src/FreshMvvm/IPageModelCoreMethods.cs +++ b/src/FreshMvvm/IPageModelCoreMethods.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using System.Windows.Input; using Xamarin.Forms; namespace FreshMvvm @@ -82,6 +83,14 @@ public interface IPageModelCoreMethods void BatchBegin(); void BatchCommit(); + + ICommand CreateCommand(Func execute, SharedLock sharedLock = null); + + ICommand CreateCommand(Func execute, SharedLock sharedLock = null); + + ICommand CreateCommand(Func execute, SharedLock sharedLock = null); + + ICommand CreateCommand(Func execute, Func stringConverter, SharedLock sharedLock = null); } } diff --git a/src/FreshMvvm/PageModelCoreMethods.cs b/src/FreshMvvm/PageModelCoreMethods.cs index bd6405b..48d6269 100644 --- a/src/FreshMvvm/PageModelCoreMethods.cs +++ b/src/FreshMvvm/PageModelCoreMethods.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Xamarin.Forms; using System.Linq; +using System.Windows.Input; namespace FreshMvvm { @@ -272,6 +273,54 @@ public void RemoveFromNavigation (bool removeAll = false) where TPag } } } + + /// + /// Creates a command without parameter. + /// + /// The command. + /// Execute method. + /// Shared lock. + public ICommand CreateCommand(Func execute, SharedLock sharedLock = null) + { + return new FreshNavigationCommand(execute, sharedLock); + } + + /// + /// Creates a command with a object parameter. + /// + /// The command. + /// Execute method. + /// Shared lock. + public ICommand CreateCommand(Func execute, SharedLock sharedLock = null) + { + return new FreshNavigationCommand(execute, sharedLock); + } + + /// + /// Creates a genric command. + /// + /// The command. + /// Execute method. + /// Shared lock. + /// Parameter type, ex. string. + public ICommand CreateCommand(Func execute, SharedLock sharedLock = null) + { + return new FreshNavigationCommand(execute, sharedLock); + } + + /// + /// Creates a command, with a conversion function from string to given type. + /// Typicaly used in xaml parameters to convert command parameters from string. + /// + /// The command. + /// Execute method. + /// int.Parse, "0" to 0 + /// Shared lock. + /// Parameter type to convert to from string. + public ICommand CreateCommand(Func execute, Func stringConverter, SharedLock sharedLock = null) + { + return new FreshNavigationCommand((obj) => execute(stringConverter(obj)), sharedLock); + } } }