diff --git a/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml index a6a53c2..1f3d779 100644 --- a/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml +++ b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml @@ -37,5 +37,7 @@ + + diff --git a/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml.cs b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml.cs index 5797064..9a5d50b 100644 --- a/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml.cs +++ b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControl.xaml.cs @@ -23,7 +23,6 @@ public partial class DataGridAndListViewControl : UserControl { public DataGridAndListViewControl() { - DataContext = new DataGridTestViewModel(); InitializeComponent(); } } diff --git a/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControlAlternateBinding.xaml b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControlAlternateBinding.xaml new file mode 100644 index 0000000..de57426 --- /dev/null +++ b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControlAlternateBinding.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + ListView Bound To TestCollectionView + + DataGrid Bound To EditableTestCollectionView + + + + + + + + + + + + + + + + + + + + + diff --git a/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControlAlternateBinding.xaml.cs b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControlAlternateBinding.xaml.cs new file mode 100644 index 0000000..91c7315 --- /dev/null +++ b/ExamplesAndTests/EditableDataGridTest/Controls/DataGridAndListViewControlAlternateBinding.xaml.cs @@ -0,0 +1,29 @@ +using EditableDataGridTest.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; + +namespace EditableDataGridTest.Controls +{ + /// + /// Interaction logic for DataGridAndListViewControl.xaml + /// + public partial class DataGridAndListViewControlAlternateBinding : UserControl + { + public DataGridAndListViewControlAlternateBinding() + { + InitializeComponent(); + } + } +} diff --git a/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml b/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml index 39f4168..f41056c 100644 --- a/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml +++ b/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml @@ -7,6 +7,13 @@ mc:Ignorable="d" Title="Editable DataGrid Test" Height="400" Width="600"> - + + + + + + + + diff --git a/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml.cs b/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml.cs index 3e2982c..bc39801 100644 --- a/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml.cs +++ b/ExamplesAndTests/EditableDataGridTest/MainWindow.xaml.cs @@ -1,4 +1,5 @@ -using System; +using EditableDataGridTest.ViewModels; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -22,7 +23,8 @@ public partial class MainWindow : Window { public MainWindow() { - InitializeComponent(); + DataContext = new DataGridTestViewModel(); + InitializeComponent(); } } } diff --git a/ExamplesAndTests/EditableDataGridTest/ViewModels/DataGridTestViewModel.cs b/ExamplesAndTests/EditableDataGridTest/ViewModels/DataGridTestViewModel.cs index 3ce3826..143c3af 100644 --- a/ExamplesAndTests/EditableDataGridTest/ViewModels/DataGridTestViewModel.cs +++ b/ExamplesAndTests/EditableDataGridTest/ViewModels/DataGridTestViewModel.cs @@ -1,33 +1,88 @@ using Swordfish.NET.Collections; using Swordfish.NET.Collections.Auxiliary; using System; +using System.Collections.Generic; +using System.ComponentModel; using System.Linq; +using System.Windows.Input; +using System.Windows.Media.Animation; namespace EditableDataGridTest.ViewModels { /// /// View model used as a source of items for both a ListView and a DataGrid /// - public class DataGridTestViewModel + public class DataGridTestViewModel : INotifyPropertyChanged { private Random _random = new Random(); public DataGridTestViewModel() { - var randomItems = Enumerable.Range(0, 10).Select(i => new TestItem + TestCollection.PropertyChanged += (s, e) => { - Label = _random.Next().ToString(), - Value1 = _random.Next().ToString(), - Value2 = _random.Next().ToString() - }); + switch (e.PropertyName) + { + case nameof(ConcurrentObservableCollection.CollectionView): + PropertyChanged?.Invoke(this,new PropertyChangedEventArgs(nameof(TestCollectionView))); + break; + case nameof(ConcurrentObservableCollection.EditableCollectionView): + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditableTestCollectionView))); + break; + } + }; + + var randomItems = Enumerable.Range(0, 10).Select(i => GetRandomItem()); TestCollection.AddRange(randomItems); } + + private TestItem GetRandomItem() => new TestItem + { + Label = _random.Next().ToString(), + Value1 = _random.Next().ToString(), + Value2 = _random.Next().ToString() + }; + + private RelayCommandFactory _addRandomItemCommand = new RelayCommandFactory(); + public ICommand AddRandomItemCommand => _addRandomItemCommand.GetCommand(() => TestCollection.Add(GetRandomItem())); + /// /// The source collection /// public ConcurrentObservableCollection TestCollection { get; } = new ConcurrentObservableCollection(); + + public IList TestCollectionView => TestCollection.CollectionView; + public IList EditableTestCollectionView => TestCollection.EditableCollectionView; + + public event PropertyChangedEventHandler PropertyChanged; } +public class ExampleViewModel : INotifyPropertyChanged +{ + public ExampleViewModel() + { + TestCollection.PropertyChanged += (s, e) => + { + switch (e.PropertyName) + { + case nameof(ConcurrentObservableCollection.CollectionView): + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TestCollectionView))); + break; + case nameof(ConcurrentObservableCollection.EditableCollectionView): + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditableTestCollectionView))); + break; + } + }; + } + + public ConcurrentObservableCollection TestCollection { get; } + = new ConcurrentObservableCollection(); + + public IList TestCollectionView => TestCollection.CollectionView; + public IList EditableTestCollectionView => TestCollection.EditableCollectionView; + + public event PropertyChangedEventHandler PropertyChanged; +} + /// /// Test item used to populate the test collection /// diff --git a/ExamplesAndTests/EditableDataGridTest/ViewModels/RelayCommand.cs b/ExamplesAndTests/EditableDataGridTest/ViewModels/RelayCommand.cs new file mode 100644 index 0000000..923d900 --- /dev/null +++ b/ExamplesAndTests/EditableDataGridTest/ViewModels/RelayCommand.cs @@ -0,0 +1,317 @@ +using Swordfish.NET.Collections.Auxiliary; +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Input; + +namespace EditableDataGridTest.ViewModels +{ + /// + /// Preferable to use RelayCommandFactory to generate these + /// + public class RelayCommand : ICommand + { + #region Fields + + protected readonly Action _execute; + protected readonly Predicate _canExecute; + protected bool _isRunning = false; + + #endregion // Fields + + #region Constructors + + protected RelayCommand(Predicate canExecute) + { + _execute = null; + _canExecute = canExecute; + } + + /// + /// Internal constructor, use the RelayCommandFactory for generating instances + /// + /// + internal RelayCommand(Action execute) : this(execute, null) + { + } + + /// + /// Internal constructor, use the RelayCommandFactory for generating instances + /// + /// + /// + internal RelayCommand(Action execute, Predicate canExecute) + { + _execute = execute ?? throw new ArgumentNullException("execute"); + _canExecute = canExecute; + } + + public void OnCanExecuteChanged() + { + CommandManager.InvalidateRequerySuggested(); + } + + #endregion // Constructors + + #region ICommand Members + + [DebuggerStepThrough] + public bool CanExecute(object parameter) + { + return !_isRunning && (_canExecute?.Invoke(parameter) ?? true); + } + + public event EventHandler CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + public virtual void Execute(object parameter) + { + _isRunning = true; + CommandManager.InvalidateRequerySuggested(); + _execute(parameter); + _isRunning = false; + CommandManager.InvalidateRequerySuggested(); + } + + #endregion // ICommand Members + } + + public class RelayCommandAsync : RelayCommand + { + private Func _executeTask; + public RelayCommandAsync(Func execute) + : this(execute, null) + { + } + + public RelayCommandAsync(Func execute, Predicate canExecute) + : base(canExecute) + { + _executeTask = execute ?? throw new ArgumentNullException("execute"); + } + + public RelayCommandAsync(Action execute) + : this(execute, null) + { + } + + public RelayCommandAsync(Action execute, Predicate canExecute) + : this(parameter => Task.Factory.StartNew(() => execute(parameter)), canExecute) + { + } + + public override void Execute(object parameter) + { + var context = SynchronizationContext.Current; + _isRunning = true; + CommandManager.InvalidateRequerySuggested(); + _executeTask(parameter).ContinueWith((t) => + { + _isRunning = false; + context?.Post(x => CommandManager.InvalidateRequerySuggested(), null); + }); + } + + } + + + /// + /// Preferred way of creating RelayCommand objects + /// + /// + /// private RelayCommandFactory _loadFilesCommand = new RelayCommandFactory(); + /// public ICommand LoadFilesCommand => + /// _loadFilesCommand.GetCommandAsync(async () => + /// { + /// OpenFileDialog dialog = new OpenFileDialog(); + /// dialog.Multiselect = true; + /// if (dialog.ShowDialog() == true) + /// { + /// foreach (var filename in dialog.FileNames) + /// { + /// await LoadFile(filename); + /// } + /// } + /// MessageBox.Show("Finished Loading FIles"); + /// }); + /// } + /// + /// private RelayCommandFactory _testCommand = new RelayCommandFactory(); + /// public ICommand TestCommand => + /// _testCommand.GetCommandAsync(async () => await Task.Run(() => + /// { + /// for (int i = 0; i < 100; ++i) + /// { + /// Thread.Sleep(100); + /// Progress = i / 99.0; + /// } + /// })); + /// + /// + /// private RelayCommandFactory _removeLastCommand = new RelayCommandFactory(); + /// public ICommand RemoveLastCommand => + /// _removeLastCommand.GetCommandAsync(async () => + /// { + /// var itemRemoved = await Task.Run(() => TestCollection.RemoveLast()); + /// Message($"Removed {itemRemoved}"); + /// }); + /// + /// // The first example can also be done like this without the task and async + /// + /// private RelayCommandFactory _testCommand = new RelayCommandFactory(); + /// public ICommand TestCommand => + /// _testCommand.GetCommandAsync(() => + /// { + /// for (int i = 0; i < 100; ++i) + /// { + /// Thread.Sleep(100); + /// Progress = i / 99.0; + /// } + /// })); + /// + public class RelayCommandFactory + { + private RelayCommand _relayCommand = null; + + public RelayCommandFactory() + { + + } + + /// + /// Gets the command and blocks execution while the command is running + /// + /// + /// + /// + public RelayCommand GetCommand(Action execute, Predicate canExecute) + { + _relayCommand = _relayCommand ?? new RelayCommand(execute, canExecute); + return _relayCommand; + } + public RelayCommand GetCommand(Action execute, Predicate canExecute) + { + return GetCommand(param => execute(), canExecute); + } + public RelayCommand GetCommand(Action execute, Func canExecute) + { + return GetCommand(execute, param => canExecute()); + } + public RelayCommand GetCommand(Action execute, Func canExecute) + { + return GetCommand(param => execute(), param => canExecute()); + } + public RelayCommand GetCommand(Action execute) + { + return GetCommand(execute, (Predicate)null); + } + public RelayCommand GetCommand(Action execute) + { + return GetCommand(param => execute(), (Predicate)null); + } + public RelayCommand GetCommandAsync(Action execute, Predicate canExecute) + { + _relayCommand = _relayCommand ?? new RelayCommandAsync(execute, canExecute); + return _relayCommand; + } + public RelayCommand GetCommandAsync(Action execute, Predicate canExecute) + { + return GetCommandAsync(param => execute(), canExecute); + } + public RelayCommand GetCommandAsync(Action execute, Func canExecute) + { + return GetCommandAsync(execute, param => canExecute()); + } + public RelayCommand GetCommandAsync(Action execute, Func canExecute) + { + return GetCommandAsync(param => execute(), param => canExecute()); + } + public RelayCommand GetCommandAsync(Action execute) + { + return GetCommandAsync(execute, (Predicate)null); + } + public RelayCommand GetCommandAsync(Action execute) + { + return GetCommandAsync(param => execute(), (Predicate)null); + } + + + public RelayCommand GetCommandAsync(Func execute, Predicate canExecute) + { + _relayCommand = _relayCommand ?? new RelayCommandAsync(execute, canExecute); + return _relayCommand; + } + public RelayCommand GetCommandAsync(Func execute, Predicate canExecute) + { + return GetCommandAsync(param => execute(), canExecute); + } + public RelayCommand GetCommandAsync(Func execute, Func canExecute) + { + return GetCommandAsync(execute, param => canExecute()); + } + public RelayCommand GetCommandAsync(Func execute, Func canExecute) + { + return GetCommandAsync(param => execute(), param => canExecute()); + } + public RelayCommand GetCommandAsync(Func execute) + { + return GetCommandAsync(execute, (Predicate)null); + } + public RelayCommand GetCommandAsync(Func execute) + { + return GetCommandAsync(param => execute(), (Predicate)null); + } + + } + + public class RelayCommandTest : ExtendedNotifyPropertyChanged + { + public static RelayCommandTest PreviousInstance { get; set; } + public static RelayCommandTest NewInstance + { + get + { + PreviousInstance = new RelayCommandTest(); + return PreviousInstance; + } + } + + private RelayCommandFactory _testCommandAsync = new RelayCommandFactory(); + public ICommand TestCommandAsync => + _testCommandAsync.GetCommandAsync(() => + { + for (int i = 0; i < 100; ++i) + { + Thread.Sleep(100); + Progress = i / 99.0; + } + }); + + private RelayCommandFactory _testCommand = new RelayCommandFactory(); + public ICommand TestCommand => _testCommand.GetCommand(param => + { + for (int i = 0; i < 100; ++i) + { + Thread.Sleep(10); + Progress = i / 99.0; + } + }); + + private double _progress = 0; + public double Progress + { + get + { + return _progress; + } + set + { + SetProperty(ref _progress, value); + } + } + } +} diff --git a/README.md b/README.md index 819913b..6b7410f 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,49 @@ on the collection. Here's the code, which hopefully makes it clear: ``` +For the above xaml you would have a view model that looks like this: + +```csharp +public class ExampleViewModel : INotifyPropertyChanged +{ + public ConcurrentObservableCollection TestCollection { get; } + = new ConcurrentObservableCollection(); + + public event PropertyChangedEventHandler PropertyChanged; +} +``` + +There is an alternative way to bind to the collection. In your view model you +subscribe to the `PropertyChanged` event of the collections you are using, +and you pass the CollectionView through another property in your view model. +Here's and example of a view model that does this: +```csharp +public class ExampleViewModel : INotifyPropertyChanged +{ + public ExampleViewModel() + { + TestCollection.PropertyChanged += (s, e) => + { + switch (e.PropertyName) + { + case nameof(ConcurrentObservableCollection.CollectionView): + PropertyChanged?.Invoke(this, + new PropertyChangedEventArgs(nameof(TestCollectionView))); + break; + } + }; + } + + public ConcurrentObservableCollection TestCollection { get; } + = new ConcurrentObservableCollection(); + + public IList TestCollectionView => TestCollection.CollectionView; + + public event PropertyChangedEventHandler PropertyChanged; +} + +``` + # Usage (EditableCollectionView) - v3.3.0 onwards The `EditableCollectionView` property is new for version 3.3.0 and is only on @@ -126,6 +169,74 @@ found in this repo. ``` +For the xaml above you would have a view model that looks like this: +```csharp +public class ExampleViewModel +{ + public ExampleViewModel() + { + } + + public ConcurrentObservableCollection TestCollection { get; } + = new ConcurrentObservableCollection(); +} +``` + +Alternatively, same as for binding to CollectionView (see previous section), you listen +to the `PropertyChanged` event on the collection and have a proxy for the `EditableCollectionView` +property like the example below, which includes proxies for `CollectionView` and `EditableCollectionView` + +```csharp +public class ExampleViewModel : INotifyPropertyChanged +{ + public ExampleViewModel() + { + TestCollection.PropertyChanged += (s, e) => + { + switch (e.PropertyName) + { + case nameof(ConcurrentObservableCollection.CollectionView): + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TestCollectionView))); + break; + case nameof(ConcurrentObservableCollection.EditableCollectionView): + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditableTestCollectionView))); + break; + } + }; + } + + public ConcurrentObservableCollection TestCollection { get; } + = new ConcurrentObservableCollection(); + + public IList TestCollectionView => TestCollection.CollectionView; + public IList EditableTestCollectionView => TestCollection.EditableCollectionView; + + public event PropertyChangedEventHandler PropertyChanged; +} + +``` + +For the above view model your xaml could look something like this: + +```xml + + + + + + + + + + + + + + + + +``` + Below is a screenshot of the EditableDataGridTest example running, on the right is the DataGrid control with the dummy row being displayed indicating a new item can be added to the collection from the DataGrid control. diff --git a/Swordfish.NET.CollectionsV3/ConcurrentObservableCollection.cs b/Swordfish.NET.CollectionsV3/ConcurrentObservableCollection.cs index dfa0cd8..f7ccaa0 100644 --- a/Swordfish.NET.CollectionsV3/ConcurrentObservableCollection.cs +++ b/Swordfish.NET.CollectionsV3/ConcurrentObservableCollection.cs @@ -53,6 +53,13 @@ public ConcurrentObservableCollection() : this(true) public ConcurrentObservableCollection(bool isMultithreaded) : base(isMultithreaded, ImmutableList.Empty) { _editableCollectionView = EditableImmutableListBridge.Empty(this); + PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(CollectionView)) + { + RaisePropertyChanged(nameof(EditableCollectionView)); + } + }; } protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs changes)