diff --git a/IListItemConverter.cs b/IListItemConverter.cs deleted file mode 100644 index 6fbb349..0000000 --- a/IListItemConverter.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace FunctionalFun.UI.Behaviours -{ - /// - /// Converts items in the Master list to Items in the target list, and back again. - /// - public interface IListItemConverter - { - /// - /// Converts the specified master list item. - /// - /// The master list item. - /// The result of the conversion. - object Convert(object masterListItem); - - /// - /// Converts the specified target list item. - /// - /// The target list item. - /// The result of the conversion. - object ConvertBack(object targetListItem); - } -} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 68a49da..0000000 --- a/LICENSE +++ /dev/null @@ -1,24 +0,0 @@ -This is free and unencumbered software released into the public domain. - -Anyone is free to copy, modify, publish, use, compile, sell, or -distribute this software, either in source code form or as a compiled -binary, for any purpose, commercial or non-commercial, and by any -means. - -In jurisdictions that recognize copyright laws, the author or authors -of this software dedicate any and all copyright interest in the -software to the public domain. We make this dedication for the benefit -of the public at large and to the detriment of our heirs and -successors. We intend this dedication to be an overt act of -relinquishment in perpetuity of all present and future rights to this -software under copyright law. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR -OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -For more information, please refer to diff --git a/License.txt b/License.txt new file mode 100644 index 0000000..d17e443 --- /dev/null +++ b/License.txt @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) SelectedItemsSynchronizer contributors. (SelectedItemsSynchronizer@prims.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/MultiSelectorBehaviours.cs b/MultiSelectorBehaviours.cs deleted file mode 100644 index 2e7b907..0000000 --- a/MultiSelectorBehaviours.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Collections; -using System.Windows; -using System.Windows.Controls.Primitives; -using System.Windows.Controls; -using System; - -namespace FunctionalFun.UI.Behaviours -{ - /// - /// A sync behaviour for a multiselector. - /// - public static class MultiSelectorBehaviours - { - public static readonly DependencyProperty SynchronizedSelectedItems = DependencyProperty.RegisterAttached( - "SynchronizedSelectedItems", typeof(IList), typeof(MultiSelectorBehaviours), new PropertyMetadata(null, OnSynchronizedSelectedItemsChanged)); - - private static readonly DependencyProperty SynchronizationManagerProperty = DependencyProperty.RegisterAttached( - "SynchronizationManager", typeof(SynchronizationManager), typeof(MultiSelectorBehaviours), new PropertyMetadata(null)); - - /// - /// Gets the synchronized selected items. - /// - /// The dependency object. - /// The list that is acting as the sync list. - public static IList GetSynchronizedSelectedItems(DependencyObject dependencyObject) - { - return (IList) dependencyObject.GetValue(SynchronizedSelectedItems); - } - - /// - /// Sets the synchronized selected items. - /// - /// The dependency object. - /// The value to be set as synchronized items. - public static void SetSynchronizedSelectedItems(DependencyObject dependencyObject, IList value) - { - dependencyObject.SetValue(SynchronizedSelectedItems, value); - } - - private static SynchronizationManager GetSynchronizationManager(DependencyObject dependencyObject) - { - return (SynchronizationManager) dependencyObject.GetValue(SynchronizationManagerProperty); - } - - private static void SetSynchronizationManager(DependencyObject dependencyObject, SynchronizationManager value) - { - dependencyObject.SetValue(SynchronizationManagerProperty, value); - } - - private static void OnSynchronizedSelectedItemsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) - { - if (e.OldValue != null) - { - SynchronizationManager synchronizer = GetSynchronizationManager(dependencyObject); - synchronizer.StopSynchronizing(); - - SetSynchronizationManager(dependencyObject, null); - } - - IList list = e.NewValue as IList; - Selector selector = dependencyObject as Selector; - - // check that this property is an IList, and that it is being set on a ListBox - if (list != null && selector != null) - { - SynchronizationManager synchronizer = GetSynchronizationManager(dependencyObject); - if (synchronizer == null) - { - synchronizer = new SynchronizationManager(selector); - SetSynchronizationManager(dependencyObject, synchronizer); - } - - synchronizer.StartSynchronizingList(); - } - } - - /// - /// A synchronization manager. - /// - private class SynchronizationManager - { - private readonly Selector _multiSelector; - private TwoListSynchronizer _synchronizer; - - /// - /// Initializes a new instance of the class. - /// - /// The selector. - internal SynchronizationManager(Selector selector) - { - _multiSelector = selector; - } - - /// - /// Starts synchronizing the list. - /// - public void StartSynchronizingList() - { - IList list = GetSynchronizedSelectedItems(_multiSelector); - - if (list != null) - { - _synchronizer = new TwoListSynchronizer(GetSelectedItemsCollection(_multiSelector), list); - _synchronizer.StartSynchronizing(); - } - } - - /// - /// Stops synchronizing the list. - /// - public void StopSynchronizing() - { - _synchronizer.StopSynchronizing(); - } - - public static IList GetSelectedItemsCollection(Selector selector) - { - if (selector is MultiSelector) - { - return (selector as MultiSelector).SelectedItems; - } - else if (selector is ListBox) - { - return (selector as ListBox).SelectedItems; - } - else - { - throw new InvalidOperationException("Target object has no SelectedItems property to bind."); - } - } - - } - } -} diff --git a/NextVersion.txt b/NextVersion.txt new file mode 100644 index 0000000..9325c3c --- /dev/null +++ b/NextVersion.txt @@ -0,0 +1 @@ +0.3.0 \ No newline at end of file diff --git a/Psync.xcf b/Psync.xcf new file mode 100644 index 0000000..8a971ff Binary files /dev/null and b/Psync.xcf differ diff --git a/Psync128.png b/Psync128.png new file mode 100644 index 0000000..5f64c23 Binary files /dev/null and b/Psync128.png differ diff --git a/Psync256.png b/Psync256.png new file mode 100644 index 0000000..f24a7ca Binary files /dev/null and b/Psync256.png differ diff --git a/README.md b/README.md index ed55975..afafce4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,101 @@ +[![][build-img]][build] +[![][nuget-img]][nuget] + +[build]: https://ci.appveyor.com/project/9swampy/selecteditemsbindingdemo +[build-img]: https://ci.appveyor.com/api/projects/status/aa8mw3opmlyxc68u?svg=true + +[nuget]: https://badge.fury.io/nu/SelectedItemsSynchronizer +[nuget-img]: https://badge.fury.io/nu/SelectedItemsSynchronizer.svg + SelectedItemsBindingDemo ======================== An example of how to bind the SelectedItems property of an ItemsControl in WPF to a ViewModel + +The origin of all this and a good explanation of how it all works can be found at +http://blog.functionalfun.net/2009/02/how-to-databind-to-selecteditems.html + +The original example demo is still included in the code base, but the code has been reorganised a bit so that it can be published as a utility library in a NuGet package. I'm hoping you'll find it as useful as I have done, and making it available as a NuGet package should ease integration with your production code! + +Example usage +------------- +```C# +List names; +ObservableCollection selectedNames; +ListView listView; + +[TestInitialize] +public void TestInitialize() +{ + names = new List() { "Abraham", "Lincoln", "James", "Buchanan" }; + selectedNames = new ObservableCollection(); + listView = new ListView(); + listView.ItemsSource = names; + listView.SelectionMode = SelectionMode.Extended; + MultiSelectorBehaviours.SetSynchronizedSelectedItems(listView, (IList)selectedNames); +} + +[TestMethod] +public void InitialiseToNoSelection() +{ + this.selectedNames.Count().Should().Be(0); +} + +[TestMethod] +public void ShouldSynchroniseListViewSelectAll() +{ + // Act + this.listView.SelectAll(); + + // Assert + this.selectedNames.Count().Should().Be(this.names.Count()); +} + +[TestMethod] +public void ShouldSynchroniseListViewSetSelectedIndex() +{ + // Act + this.listView.SelectedIndex = 0; + + // Assert + this.selectedNames.Count().Should().Be(1); +} + +[TestMethod] +public void ShouldSynchroniseListViewSetSelectedItem() +{ + //Arrange + Random random = new Random(DateTime.Now.Millisecond); + object itemToSelect = this.listView.Items.GetItemAt(random.Next(this.names.Count)); + + // Act + this.listView.SelectedItem = itemToSelect; + + // Assert + this.selectedNames.Count().Should().Be(1); +} + +[TestMethod] +public void ShouldSynchroniseListViewAddSingleSelectedItem() +{ + //Arrange + Random random = new Random(DateTime.Now.Millisecond); + object itemToSelect = this.listView.Items.GetItemAt(random.Next(this.names.Count)); + + // Act + this.listView.SelectedItems.Add(itemToSelect); + + // Assert + this.selectedNames.Count().Should().Be(1); +} + +[TestMethod] +public void ShouldSynchroniseListViewAddMultipleSelectedItems() +{ + // Act + this.listView.SelectedItems.Add(this.listView.Items.GetItemAt(0)); + this.listView.SelectedItems.Add(this.listView.Items.GetItemAt(1)); + + // Assert + this.selectedNames.ShouldBeEquivalentTo(this.listView.SelectedItems, opt => opt.WithStrictOrdering()); +} diff --git a/SelectedItemsBindingDemo.sln b/SelectedItemsBindingDemo.sln deleted file mode 100644 index 8a34742..0000000 --- a/SelectedItemsBindingDemo.sln +++ /dev/null @@ -1,20 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 10.00 -# Visual C# Express 2008 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SelectedItemsBindingDemo", "SelectedItemsBindingDemo.csproj", "{7DA5A107-B474-4AC0-B861-63A489DB0C02}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection -EndGlobal diff --git a/App.xaml b/SelectedItemsBindingDemo/App.xaml similarity index 100% rename from App.xaml rename to SelectedItemsBindingDemo/App.xaml diff --git a/App.xaml.cs b/SelectedItemsBindingDemo/App.xaml.cs similarity index 100% rename from App.xaml.cs rename to SelectedItemsBindingDemo/App.xaml.cs diff --git a/Properties/AssemblyInfo.cs b/SelectedItemsBindingDemo/Properties/AssemblyInfo.cs similarity index 100% rename from Properties/AssemblyInfo.cs rename to SelectedItemsBindingDemo/Properties/AssemblyInfo.cs diff --git a/Properties/Resources.Designer.cs b/SelectedItemsBindingDemo/Properties/Resources.Designer.cs similarity index 84% rename from Properties/Resources.Designer.cs rename to SelectedItemsBindingDemo/Properties/Resources.Designer.cs index e747212..1dd8401 100644 --- a/Properties/Resources.Designer.cs +++ b/SelectedItemsBindingDemo/Properties/Resources.Designer.cs @@ -1,17 +1,17 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:2.0.50727.3082 +// Runtime Version:4.0.30319.34014 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ -namespace SelectedItemsBindingDemo.Properties -{ - - +namespace SelectedItemsBindingDemo.Properties { + using System; + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -19,51 +19,43 @@ namespace SelectedItemsBindingDemo.Properties // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "2.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - + internal class Resources { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { + internal Resources() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("SelectedItemsBindingDemo.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } - set - { + set { resourceCulture = value; } } diff --git a/Properties/Resources.resx b/SelectedItemsBindingDemo/Properties/Resources.resx similarity index 100% rename from Properties/Resources.resx rename to SelectedItemsBindingDemo/Properties/Resources.resx diff --git a/Properties/Settings.Designer.cs b/SelectedItemsBindingDemo/Properties/Settings.Designer.cs similarity index 78% rename from Properties/Settings.Designer.cs rename to SelectedItemsBindingDemo/Properties/Settings.Designer.cs index 4a09fca..f51150d 100644 --- a/Properties/Settings.Designer.cs +++ b/SelectedItemsBindingDemo/Properties/Settings.Designer.cs @@ -1,28 +1,24 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:2.0.50727.3082 +// Runtime Version:4.0.30319.34014 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // //------------------------------------------------------------------------------ -namespace SelectedItemsBindingDemo.Properties -{ - - +namespace SelectedItemsBindingDemo.Properties { + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "9.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { + + public static Settings Default { + get { return defaultInstance; } } diff --git a/Properties/Settings.settings b/SelectedItemsBindingDemo/Properties/Settings.settings similarity index 100% rename from Properties/Settings.settings rename to SelectedItemsBindingDemo/Properties/Settings.settings diff --git a/RelayCommand.cs b/SelectedItemsBindingDemo/RelayCommand.cs similarity index 100% rename from RelayCommand.cs rename to SelectedItemsBindingDemo/RelayCommand.cs diff --git a/SelectedItemsBindingDemo.csproj b/SelectedItemsBindingDemo/SelectedItemsBindingDemo.csproj similarity index 64% rename from SelectedItemsBindingDemo.csproj rename to SelectedItemsBindingDemo/SelectedItemsBindingDemo.csproj index e89874d..e85dc50 100644 --- a/SelectedItemsBindingDemo.csproj +++ b/SelectedItemsBindingDemo/SelectedItemsBindingDemo.csproj @@ -1,5 +1,5 @@  - + Debug AnyCPU @@ -10,10 +10,31 @@ Properties SelectedItemsBindingDemo SelectedItemsBindingDemo - v3.5 + v4.5 512 {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 4 + + + + + 3.5 + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true true @@ -23,6 +44,7 @@ DEBUG;TRACE prompt 4 + false pdbonly @@ -31,12 +53,15 @@ TRACE prompt 4 + false 3.5 + + 3.5 @@ -53,10 +78,14 @@ MSBuild:Compile Designer + MSBuild:Compile + Designer MSBuild:Compile Designer + MSBuild:Compile + Designer App.xaml @@ -68,8 +97,6 @@ - - Code @@ -84,18 +111,36 @@ True - ResXFileCodeGenerator Resources.Designer.cs + SettingsSingleFileGenerator Settings.Designer.cs + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + true + + + + + {558e4d9f-ccb2-4da8-82b3-3ad41f4637e4} + PrimS.SelectedItemsSynchronizer.Net45 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Window1.xaml.cs b/SelectedItemsBindingDemo/Window1.xaml.cs similarity index 100% rename from Window1.xaml.cs rename to SelectedItemsBindingDemo/Window1.xaml.cs diff --git a/SelectedItemsBindingDemo/app.config b/SelectedItemsBindingDemo/app.config new file mode 100644 index 0000000..51278a4 --- /dev/null +++ b/SelectedItemsBindingDemo/app.config @@ -0,0 +1,3 @@ + + + diff --git a/SelectedItemsSynchronizer.0.5.1-beta.nupkg b/SelectedItemsSynchronizer.0.5.1-beta.nupkg new file mode 100644 index 0000000..8a7c039 Binary files /dev/null and b/SelectedItemsSynchronizer.0.5.1-beta.nupkg differ diff --git a/SelectedItemsSynchronizer.CiTests/MultiSelectorBehavioursCalendarTests.cs b/SelectedItemsSynchronizer.CiTests/MultiSelectorBehavioursCalendarTests.cs new file mode 100644 index 0000000..9aa2c98 --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/MultiSelectorBehavioursCalendarTests.cs @@ -0,0 +1,81 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using System; + using System.Collections; + using System.Collections.ObjectModel; + using System.Linq; + using System.Windows.Controls; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class MultiSelectorBehavioursCalendarTests + { + ObservableCollection selectedDates; + Calendar calendar; + + [TestInitialize] + public void TestInitialize() + { + selectedDates = new ObservableCollection(); + calendar = new Calendar(); + calendar.SelectionMode = CalendarSelectionMode.SingleRange; + MultiSelectorBehaviours.SetSynchronizedSelectedItems(calendar, (IList)selectedDates); + } + + [TestMethod] + public void InitialiseToNoSelection() + { + this.selectedDates.Count().Should().Be(0); + } + + [TestMethod] + public void SelectInCollectionShouldReflectOnCalendar() + { + // Arrange + this.calendar.MonitorEvents(); + + // Act + this.selectedDates.Add(DateTime.Today); + + // Assert + this.calendar.ShouldRaise("SelectedDatesChanged"); + } + + [TestMethod] + public void SelectOnCalendarShouldReflectInCollection() + { + // Act + this.calendar.SelectedDates.Add(DateTime.Today); + + // Assert + this.selectedDates.Count().Should().Be(1); + } + + [TestMethod] + public void SelectConsecutiveOnCalendarShouldReflectInCollection() + { + // Act + this.calendar.SelectedDates.AddRange(DateTime.Today, DateTime.Today.AddDays(1)); + + // Assert + this.selectedDates.Count().Should().Be(2); + } + + [Ignore] + [TestMethod] + public void SelectConsecutiveInCollectionShouldReflectOnCalendar() + { + //This test is failing to highlight the fact that the implementation currently only works OneWayToSource due to the + //odd behaviour of SelectedDateCollection.Add. + + // Act + this.selectedDates.Add(DateTime.Today); + this.selectedDates.Add(DateTime.Today.AddDays(1)); + + // Assert + this.selectedDates.Count().Should().Be(2); + this.calendar.SelectedDates.Count().Should().Be(2); + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/MultiSelectorBehavioursListViewTests.cs b/SelectedItemsSynchronizer.CiTests/MultiSelectorBehavioursListViewTests.cs new file mode 100644 index 0000000..7968edf --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/MultiSelectorBehavioursListViewTests.cs @@ -0,0 +1,136 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using System; + using System.Collections; + using System.Collections.Generic; + using System.Collections.ObjectModel; + using System.Linq; + using System.Windows.Controls; + + [TestClass] + public class MultiSelectorBehavioursListViewTests + { + List names; + ObservableCollection selectedNames; + ListView listView; + + [TestInitialize] + public void TestInitialize() + { + names = new List() { "Abraham", "Lincoln", "James", "Buchanan" }; + selectedNames = new ObservableCollection(); + listView = new ListView(); + listView.ItemsSource = names; + listView.SelectionMode = SelectionMode.Extended; + MultiSelectorBehaviours.SetSynchronizedSelectedItems(listView, (IList)selectedNames); + } + + [TestMethod] + public void InitialiseToNoSelection() + { + this.selectedNames.Count().Should().Be(0); + } + + [TestMethod] + public void ShouldSynchroniseListViewSelectAll() + { + // Act + this.listView.SelectAll(); + + // Assert + this.selectedNames.Count().Should().Be(this.names.Count()); + } + + [TestMethod] + public void ShouldSynchroniseListViewSetSelectedIndex() + { + // Act + this.listView.SelectedIndex = 0; + + // Assert + this.selectedNames.Count().Should().Be(1); + } + + [TestMethod] + public void ShouldSynchroniseListViewSetSelectedItem() + { + //Arrange + Random random = new Random(DateTime.Now.Millisecond); + object itemToSelect = this.listView.Items.GetItemAt(random.Next(this.names.Count)); + + // Act + this.listView.SelectedItem = itemToSelect; + + // Assert + this.selectedNames.Count().Should().Be(1); + } + + [TestMethod] + public void ShouldSynchroniseListViewAddSingleSelectedItem() + { + //Arrange + Random random = new Random(DateTime.Now.Millisecond); + object itemToSelect = this.listView.Items.GetItemAt(random.Next(this.names.Count)); + + // Act + this.listView.SelectedItems.Add(itemToSelect); + + // Assert + this.selectedNames.Count().Should().Be(1); + } + + [TestMethod] + public void ShouldSynchroniseListViewAddMultipleSelectedItems() + { + // Act + this.listView.SelectedItems.Add(this.listView.Items.GetItemAt(0)); + this.listView.SelectedItems.Add(this.listView.Items.GetItemAt(1)); + + // Assert + this.selectedNames.ShouldBeEquivalentTo(this.listView.SelectedItems, opt => opt.WithStrictOrdering()); + } + + [TestMethod] + public void ShouldSynchroniseListAddMultipleSelectedItems() + { + // Act + this.selectedNames.Add(this.names.First()); + this.selectedNames.Add(this.names.Last()); + + // Assert + this.listView.SelectedItems.ShouldBeEquivalentTo(this.selectedNames, opt => opt.WithStrictOrdering()); + } + + [TestMethod] + public void ShouldNotSynchroniseListAddMultipleSelectedItems() + { + //Arrange + ObservableCollection secondSelectedNames = new ObservableCollection(); + + // Act + MultiSelectorBehaviours.SetSynchronizedSelectedItems(listView, (IList)secondSelectedNames); + this.selectedNames.Add(this.names.First()); + this.selectedNames.Add(this.names.Last()); + + // Assert + this.listView.SelectedItems.Should().NotBeEquivalentTo(this.selectedNames); + } + + [TestMethod] + public void ShouldSynchroniseChangedListAddMultipleSelectedItems() + { + //Arrange + ObservableCollection secondSelectedNames = new ObservableCollection(); + + // Act + MultiSelectorBehaviours.SetSynchronizedSelectedItems(listView, (IList)secondSelectedNames); + secondSelectedNames.Add(this.names.First()); + secondSelectedNames.Add(this.names.Last()); + + // Assert + this.listView.SelectedItems.ShouldBeEquivalentTo(secondSelectedNames); + } + } +} diff --git a/SelectedItemsSynchronizer.CiTests/PrimS.SelectedItemsSynchronizer.CiTests.csproj b/SelectedItemsSynchronizer.CiTests/PrimS.SelectedItemsSynchronizer.CiTests.csproj new file mode 100644 index 0000000..02b30ed --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/PrimS.SelectedItemsSynchronizer.CiTests.csproj @@ -0,0 +1,113 @@ + + + + Debug + AnyCPU + {918FE624-1E12-48BE-A3F0-FC1B5CF1412F} + Library + Properties + PrimS.SelectedItemsSynchronizer.CiTests + PrimS.SelectedItemsSynchronizer.CiTests + v4.5.1 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\FluentAssertions.4.0.0\lib\net45\FluentAssertions.dll + + + False + ..\packages\FluentAssertions.4.0.0\lib\net45\FluentAssertions.Core.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {6fd3314f-9a43-4fcf-a073-6c97ca2d1e81} + PrimS.SelectedItemsSynchronizer + + + + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/Properties/AssemblyInfo.cs b/SelectedItemsSynchronizer.CiTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e9aabdd --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PrimS.SelectedItemsSynchronizer.CiTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PrimS.SelectedItemsSynchronizer.CiTests")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("5726167d-d6a5-4e8b-930f-e5d3009afd66")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerAdditionBehaviour.cs b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerAdditionBehaviour.cs new file mode 100644 index 0000000..769d152 --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerAdditionBehaviour.cs @@ -0,0 +1,65 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using System; + using System.Collections.ObjectModel; + using System.Linq; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class TwoListSynchronizerAdditionBehaviour + { + private ObservableCollection masterList; + private ObservableCollection targetList; + private TwoListSynchronizer sut; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + } + + [TestInitialize] + public void TestInitialize() + { + this.masterList = new ObservableCollection(); + this.targetList = new ObservableCollection(); + this.sut = new TwoListSynchronizer(this.masterList, this.targetList); + } + + [TestMethod] + public void ShouldNotSynchroniseAddition() + { + // Act + this.masterList.Add("testString"); + + // Assert + this.targetList.Count().Should().Be(0); + } + + [TestMethod] + public void ShouldSynchroniseAddition() + { + // Arrange + this.sut.StartSynchronizing(); + + // Act + this.masterList.Add("testString"); + + // Assert + this.targetList.Count().Should().Be(1); + } + + [TestMethod] + public void ShouldSynchroniseAdditionOnTarget() + { + // Arrange + this.sut.StartSynchronizing(); + + // Act + this.targetList.Add("testString"); + + // Assert + this.masterList.Count().Should().Be(1); + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerDeletionBehaviour.cs b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerDeletionBehaviour.cs new file mode 100644 index 0000000..8e68ab2 --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerDeletionBehaviour.cs @@ -0,0 +1,118 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using System.Collections.ObjectModel; + using System.Linq; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class TwoListSynchronizerDeletionBehaviour + { + private ObservableCollection masterList; + private ObservableCollection targetList; + private TwoListSynchronizer sut; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + } + + [TestInitialize] + public void TestInitialize() + { + this.masterList = new ObservableCollection(); + this.masterList.Add("InitialisedFirst"); + this.masterList.Add("InitialisedSecond"); + this.targetList = new ObservableCollection(); + this.sut = new TwoListSynchronizer(this.masterList, this.targetList); + this.sut.StartSynchronizing(); + } + + [TestMethod] + public void ShouldBeSynchronised() + { + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList, opt => opt.WithStrictOrdering()); + } + + [TestMethod] + public void ShouldNotSynchroniseDeletion() + { + // Arrange + this.sut.StopSynchronizing(); + + // Act + this.masterList.Remove("InitialisedFirst"); + + // Assert + this.targetList.Count().Should().Be(this.masterList.Count() + 1); + } + + [TestMethod] + public void ShouldNotSynchroniseDeletionByIndex() + { + // Arrange + this.sut.StopSynchronizing(); + + // Act + this.masterList.RemoveAt(0); + + // Assert + this.targetList.Count().Should().Be(this.masterList.Count() + 1); + } + + [TestMethod] + public void ShouldSynchroniseDeletion() + { + // Act + this.masterList.Remove("InitialisedFirst"); + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList); + } + + [TestMethod] + public void ShouldSynchroniseDeletionByIndex() + { + // Act + this.masterList.RemoveAt(0); + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList); + } + + [TestMethod] + public void ShouldNotSynchroniseClearing() + { + // Arrange + this.sut.StopSynchronizing(); + int priorCount = this.masterList.Count(); + + // Act + this.masterList.Clear(); + + // Assert + this.targetList.Count().Should().Be(priorCount); + } + + [TestMethod] + public void ShouldSynchroniseClearing() + { + // Act + this.masterList.Clear(); + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList); + } + + [TestMethod] + public void ShouldSynchroniseClearingOfTarget() + { + // Act + this.targetList.Clear(); + + // Assert + this.masterList.ShouldBeEquivalentTo(this.targetList); + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerInitialization.cs b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerInitialization.cs new file mode 100644 index 0000000..3b3682b --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerInitialization.cs @@ -0,0 +1,37 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using System.Collections.ObjectModel; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class TwoListSynchronizerInitialization + { + [TestMethod] + public void ShouldCopyInitialMembersToTarget() + { + ObservableCollection masterList = new ObservableCollection(); + masterList.Add("Initial"); + ObservableCollection targetList = new ObservableCollection(); + + TwoListSynchronizer sut = new TwoListSynchronizer(masterList, targetList); + sut.StartSynchronizing(); + + targetList.ShouldBeEquivalentTo(masterList, "ctor should initialise targetList."); + } + + [TestMethod] + public void ShouldCopyInitialMembersFromTarget() + { + ObservableCollection masterList = new ObservableCollection(); + ObservableCollection targetList = new ObservableCollection(); + targetList.Add("Initial"); + + TwoListSynchronizer sut = new TwoListSynchronizer(masterList, targetList); + sut.StartSynchronizing(); + + targetList.ShouldBeEquivalentTo(masterList, "ctor should initialise targetList."); + targetList.Should().BeEmpty("ctor should overwrite targetList with masterList content."); + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerInsertionBehaviour.cs b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerInsertionBehaviour.cs new file mode 100644 index 0000000..bed3d20 --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerInsertionBehaviour.cs @@ -0,0 +1,70 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using System.Collections.ObjectModel; + using System.Linq; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class TwoListSynchronizerInsertionBehaviour + { + private ObservableCollection masterList; + private ObservableCollection targetList; + private TwoListSynchronizer sut; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + } + + [TestInitialize] + public void TestInitialize() + { + this.masterList = new ObservableCollection(); + this.masterList.Add("Initialised"); + this.targetList = new ObservableCollection(); + this.sut = new TwoListSynchronizer(this.masterList, this.targetList); + this.sut.StartSynchronizing(); + } + + [TestMethod] + public void ShouldBeSynchronised() + { + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList); + } + + [TestMethod] + public void ShouldNotSynchroniseInsertion() + { + // Arrange + this.sut.StopSynchronizing(); + + // Act + this.masterList.Insert(0, "Insertion"); + + // Assert + this.targetList.Count().Should().Be(this.masterList.Count() - 1); + } + + [TestMethod] + public void ShouldSynchroniseInsertion() + { + // Act + this.masterList.Insert(0, "Insertion"); + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList, opt => opt.WithStrictOrdering()); + } + + [TestMethod] + public void ShouldSynchroniseInsertionOnTarget() + { + // Act + this.targetList.Insert(0, "Insertion"); + + // Assert + this.masterList.ShouldBeEquivalentTo(this.targetList, opt => opt.WithStrictOrdering()); + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerMoveBehaviour.cs b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerMoveBehaviour.cs new file mode 100644 index 0000000..24ab3f1 --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerMoveBehaviour.cs @@ -0,0 +1,72 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using System.Collections.ObjectModel; + using System.Linq; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class TwoListSynchronizerMoveBehaviour + { + private ObservableCollection masterList; + private ObservableCollection targetList; + private TwoListSynchronizer sut; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + } + + [TestInitialize] + public void TestInitialize() + { + this.masterList = new ObservableCollection(); + this.masterList.Add("InitialisedFirst"); + this.masterList.Add("InitialisedSecond"); + this.targetList = new ObservableCollection(); + this.sut = new TwoListSynchronizer(this.masterList, this.targetList); + this.sut.StartSynchronizing(); + } + + [TestMethod] + public void ShouldBeSynchronised() + { + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList); + } + + [TestMethod] + public void ShouldNotSynchroniseMove() + { + // Arrange + this.sut.StopSynchronizing(); + + // Act + this.masterList.Move(0, 1); + + // Assert + this.targetList.First().Should().Be(this.masterList.Last()); + this.targetList.ShouldBeEquivalentTo(this.masterList.Reverse(), opt => opt.WithStrictOrdering()); + } + + [TestMethod] + public void ShouldSynchroniseMove() + { + // Act + this.masterList.Move(0, 1); + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList, opt => opt.WithStrictOrdering()); + } + + [TestMethod] + public void ShouldSynchroniseMoveOnTarget() + { + // Act + this.targetList.Move(0, 1); + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList, opt => opt.WithStrictOrdering()); + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerReplacementBehaviour.cs b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerReplacementBehaviour.cs new file mode 100644 index 0000000..ece4917 --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/TwoListSynchronizerReplacementBehaviour.cs @@ -0,0 +1,70 @@ +namespace PrimS.SelectedItemsSynchronizer.CiTests +{ + using System.Collections.ObjectModel; + using FluentAssertions; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + [TestClass] + public class TwoListSynchronizerReplacementBehaviour + { + private ObservableCollection masterList; + private ObservableCollection targetList; + private TwoListSynchronizer sut; + + [ClassInitialize] + public static void ClassInitialize(TestContext context) + { + } + + [TestInitialize] + public void TestInitialize() + { + this.masterList = new ObservableCollection(); + this.masterList.Add("InitialisedFirst"); + this.masterList.Add("InitialisedSecond"); + this.targetList = new ObservableCollection(); + this.sut = new TwoListSynchronizer(this.masterList, this.targetList); + this.sut.StartSynchronizing(); + } + + [TestMethod] + public void ShouldBeSynchronised() + { + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList); + } + + [TestMethod] + public void ShouldNotSynchroniseReplacement() + { + // Arrange + this.sut.StopSynchronizing(); + + // Act + this.masterList[0] = "Replaced"; + + // Assert + this.targetList.Should().NotBeEquivalentTo(this.masterList); + } + + [TestMethod] + public void ShouldSynchroniseReplacement() + { + // Act + this.masterList[0] = "Replaced"; + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList, opt => opt.WithStrictOrdering()); + } + + [TestMethod] + public void ShouldSynchroniseReplacementOnTarget() + { + // Act + this.targetList[0] = "Replaced"; + + // Assert + this.targetList.ShouldBeEquivalentTo(this.masterList, opt => opt.WithStrictOrdering()); + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer.CiTests/packages.config b/SelectedItemsSynchronizer.CiTests/packages.config new file mode 100644 index 0000000..dec037f --- /dev/null +++ b/SelectedItemsSynchronizer.CiTests/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/SelectedItemsSynchronizer.Net45/PrimS.SelectedItemsSynchronizer.Net45.csproj b/SelectedItemsSynchronizer.Net45/PrimS.SelectedItemsSynchronizer.Net45.csproj new file mode 100644 index 0000000..9b6b9a6 --- /dev/null +++ b/SelectedItemsSynchronizer.Net45/PrimS.SelectedItemsSynchronizer.Net45.csproj @@ -0,0 +1,83 @@ + + + + + Debug + AnyCPU + {558E4D9F-CCB2-4DA8-82B3-3AD41F4637E4} + Library + Properties + PrimS.SelectedItemsSynchronizer + PrimS.SelectedItemsSynchronizer.Net45 + v4.5 + 512 + 52132b50 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\PrimS.SelectedItemsSynchronizer.Net45.XML + + + + + + + + + + + + + + + + + + IListItemConverter.cs + + + ISynchronizationManager.cs + + + MultiSelectorBehaviours.cs + + + TwoListSynchronizer.cs + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/SelectedItemsSynchronizer.Net45/Properties/AssemblyInfo.cs b/SelectedItemsSynchronizer.Net45/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..787c6d0 --- /dev/null +++ b/SelectedItemsSynchronizer.Net45/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PrimS.SelectedItemsSynchronizer.NET45")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PrimS.SelectedItemsSynchronizer.NET45")] +[assembly: AssemblyCopyright("Copyright © 2015")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c3060751-daef-4bba-a3af-fdbceea2bf5c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/SelectedItemsSynchronizer.Net45/packages.config b/SelectedItemsSynchronizer.Net45/packages.config new file mode 100644 index 0000000..818b649 --- /dev/null +++ b/SelectedItemsSynchronizer.Net45/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/SelectedItemsSynchronizer.nuspec b/SelectedItemsSynchronizer.nuspec new file mode 100644 index 0000000..17b61e3 --- /dev/null +++ b/SelectedItemsSynchronizer.nuspec @@ -0,0 +1,27 @@ + + + + SelectedItemsSynchronizer + 0.5.0-beta + SelectedItemsSynchronizer + SelectedItemsSynchronizer contributors + 9swampy + https://raw.githubusercontent.com/9swampy/SelectedItemsBindingDemo/SelectedItemsSynchronizer/License.txt + https://github.com/9swampy/SelectedItemsBindingDemo/tree/SelectedItemsSynchronizer + https://raw.githubusercontent.com/9swampy/SelectedItemsBindingDemo/SelectedItemsSynchronizer/Psync128.png + false + Facilitates two-way binding of the readonly MultiSelector.SelectedItems property in ListBox/ListView on WPF. There's also a working prototype/example for binding Calendar.SelectedDates using a similar mechanism. + + Add Net4.0 package. + + Copyright (c) 9swampy, SelectedItemSynchronizer contributors + SelectedItems WPF binding ViewModel MVVM multiselector selector ListView ListBox + + + + + + + + + \ No newline at end of file diff --git a/SelectedItemsSynchronizer.sln b/SelectedItemsSynchronizer.sln new file mode 100644 index 0000000..2227ff7 --- /dev/null +++ b/SelectedItemsSynchronizer.sln @@ -0,0 +1,45 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.31101.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SelectedItemsBindingDemo", "SelectedItemsBindingDemo\SelectedItemsBindingDemo.csproj", "{7DA5A107-B474-4AC0-B861-63A489DB0C02}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrimS.SelectedItemsSynchronizer", "SelectedItemsSynchronizer\PrimS.SelectedItemsSynchronizer.csproj", "{6FD3314F-9A43-4FCF-A073-6C97CA2D1E81}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrimS.SelectedItemsSynchronizer.CiTests", "SelectedItemsSynchronizer.CiTests\PrimS.SelectedItemsSynchronizer.CiTests.csproj", "{918FE624-1E12-48BE-A3F0-FC1B5CF1412F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrimS.SelectedItemsSynchronizer.Net45", "SelectedItemsSynchronizer.NET45\PrimS.SelectedItemsSynchronizer.Net45.csproj", "{558E4D9F-CCB2-4DA8-82B3-3AD41F4637E4}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionItems", "SolutionItems", "{3BA1A4D4-A636-4B35-8DD8-2EC985FC174A}" + ProjectSection(SolutionItems) = preProject + SelectedItemsSynchronizer.nuspec = SelectedItemsSynchronizer.nuspec + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7DA5A107-B474-4AC0-B861-63A489DB0C02}.Release|Any CPU.Build.0 = Release|Any CPU + {6FD3314F-9A43-4FCF-A073-6C97CA2D1E81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6FD3314F-9A43-4FCF-A073-6C97CA2D1E81}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6FD3314F-9A43-4FCF-A073-6C97CA2D1E81}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6FD3314F-9A43-4FCF-A073-6C97CA2D1E81}.Release|Any CPU.Build.0 = Release|Any CPU + {918FE624-1E12-48BE-A3F0-FC1B5CF1412F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {918FE624-1E12-48BE-A3F0-FC1B5CF1412F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {918FE624-1E12-48BE-A3F0-FC1B5CF1412F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {918FE624-1E12-48BE-A3F0-FC1B5CF1412F}.Release|Any CPU.Build.0 = Release|Any CPU + {558E4D9F-CCB2-4DA8-82B3-3AD41F4637E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {558E4D9F-CCB2-4DA8-82B3-3AD41F4637E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {558E4D9F-CCB2-4DA8-82B3-3AD41F4637E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {558E4D9F-CCB2-4DA8-82B3-3AD41F4637E4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/SelectedItemsSynchronizer.sln.GhostDoc.xml b/SelectedItemsSynchronizer.sln.GhostDoc.xml new file mode 100644 index 0000000..0c987e9 --- /dev/null +++ b/SelectedItemsSynchronizer.sln.GhostDoc.xml @@ -0,0 +1,6 @@ + + + *.min.js + jquery*.js + + diff --git a/SelectedItemsSynchronizer/IListItemConverter.cs b/SelectedItemsSynchronizer/IListItemConverter.cs new file mode 100644 index 0000000..d3ef54d --- /dev/null +++ b/SelectedItemsSynchronizer/IListItemConverter.cs @@ -0,0 +1,22 @@ +namespace PrimS.SelectedItemsSynchronizer +{ + /// + /// Converts items in the Master list to Items in the target list, and back again. + /// + public interface IListItemConverter + { + /// + /// Converts the specified master list item. + /// + /// The master list item. + /// The result of the conversion. + object Convert(object masterListItem); + + /// + /// Converts the specified target list item. + /// + /// The target list item. + /// The result of the conversion. + object ConvertBack(object targetListItem); + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer/ISynchronizationManager.cs b/SelectedItemsSynchronizer/ISynchronizationManager.cs new file mode 100644 index 0000000..771a79f --- /dev/null +++ b/SelectedItemsSynchronizer/ISynchronizationManager.cs @@ -0,0 +1,20 @@ +namespace PrimS.SelectedItemsSynchronizer +{ + using System; + + /// + /// Provides functionality to interact with the Synchronization Manager. + /// + public interface ISynchronizationManager + { + /// + /// Starts the synchronizing. + /// + void StartSynchronizing(); + + /// + /// Stops the synchronizing. + /// + void StopSynchronizing(); + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer/MultiSelectorBehaviours.cs b/SelectedItemsSynchronizer/MultiSelectorBehaviours.cs new file mode 100644 index 0000000..027feea --- /dev/null +++ b/SelectedItemsSynchronizer/MultiSelectorBehaviours.cs @@ -0,0 +1,179 @@ +namespace PrimS.SelectedItemsSynchronizer +{ + using System; + using System.Collections; + using System.Windows; + using System.Windows.Controls; + using System.Windows.Controls.Primitives; + + /// + /// A sync behaviour for a multiselector. + /// + public static class MultiSelectorBehaviours + { + /// + /// The synchronized selected items. + /// + public static readonly DependencyProperty SynchronizedSelectedItems = DependencyProperty.RegisterAttached( + "SynchronizedSelectedItems", typeof(IList), typeof(MultiSelectorBehaviours), new PropertyMetadata(null, OnSynchronizedSelectedItemsChanged)); + + private static readonly DependencyProperty SynchronizationManagerProperty = DependencyProperty.RegisterAttached( + "SynchronizationManager", typeof(ISynchronizationManager), typeof(MultiSelectorBehaviours), new PropertyMetadata(null)); + + /// + /// Gets the synchronized selected items. + /// + /// The dependency object. + /// The list that is acting as the sync list. + public static IList GetSynchronizedSelectedItems(DependencyObject dependencyObject) + { + return (IList)dependencyObject.GetValue(SynchronizedSelectedItems); + } + + /// + /// Sets the synchronized selected items. + /// + /// The dependency object. + /// The value to be set as synchronized items. + public static void SetSynchronizedSelectedItems(DependencyObject dependencyObject, IList value) + { + dependencyObject.SetValue(SynchronizedSelectedItems, value); + } + + private static ISynchronizationManager GetSynchronizationManager(DependencyObject dependencyObject) + { + return (ISynchronizationManager)dependencyObject.GetValue(SynchronizationManagerProperty); + } + + private static void SetSynchronizationManager(DependencyObject dependencyObject, ISynchronizationManager value) + { + dependencyObject.SetValue(SynchronizationManagerProperty, value); + } + + private static void OnSynchronizedSelectedItemsChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) + { + if (e.OldValue != null) + { + ISynchronizationManager synchronizer = GetSynchronizationManager(dependencyObject); + synchronizer.StopSynchronizing(); + + SetSynchronizationManager(dependencyObject, null); + } + + Calendar calendar = dependencyObject as Calendar; + if (calendar != null) + { + ISynchronizationManager synchronizer = GetSynchronizationManager(dependencyObject); + if (synchronizer == null) + { + synchronizer = new CalendarSynchronizationManager(calendar); + SetSynchronizationManager(dependencyObject, synchronizer); + } + + synchronizer.StartSynchronizing(); + } + else + { + IList list = e.NewValue as IList; + Selector selector = dependencyObject as Selector; + + // check that this property is an IList, and that it is being set on a ListBox + if (list != null && selector != null) + { + ISynchronizationManager synchronizer = GetSynchronizationManager(dependencyObject); + if (synchronizer == null) + { + synchronizer = new SelectorSynchronizationManager(selector); + SetSynchronizationManager(dependencyObject, synchronizer); + } + + synchronizer.StartSynchronizing(); + } + } + } + + private class CalendarSynchronizationManager : BaseSynchronizationManager, ISynchronizationManager + { + /// + /// Initialises a new instance of the class. + /// + /// The calendar to sync. + internal CalendarSynchronizationManager(Calendar calendar) + : base(calendar) + { + } + + protected override IList GetSelectedItemsCollection(Calendar calendar) + { + return calendar.SelectedDates; + } + } + + private abstract class BaseSynchronizationManager : ISynchronizationManager + where T : DependencyObject + { + private readonly T source; + private TwoListSynchronizer synchronizer; + + protected BaseSynchronizationManager(T source) + { + this.source = source; + } + + /// + /// Starts synchronizing the list. + /// + public void StartSynchronizing() + { + IList list = GetSynchronizedSelectedItems(this.source); + + if (list != null) + { + this.synchronizer = new TwoListSynchronizer(this.GetSelectedItemsCollection(this.source), list); + this.synchronizer.StartSynchronizing(); + } + } + + /// + /// Stops synchronizing the list. + /// + public void StopSynchronizing() + { + this.synchronizer.StopSynchronizing(); + } + + protected abstract IList GetSelectedItemsCollection(T source); + } + + /// + /// A synchronization manager. + /// + private class SelectorSynchronizationManager : BaseSynchronizationManager + { + /// + /// Initialises a new instance of the class. + /// + /// The selector. + internal SelectorSynchronizationManager(Selector selector) + : base(selector) + { + } + + protected override IList GetSelectedItemsCollection(Selector selector) + { + if (selector is MultiSelector) + { + return (selector as MultiSelector).SelectedItems; + } + else if (selector is ListBox) + { + return (selector as ListBox).SelectedItems; + } + else + { + throw new InvalidOperationException("Target object has no SelectedItems property to bind."); + } + } + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer/PrimS.SelectedItemsSynchronizer.csproj b/SelectedItemsSynchronizer/PrimS.SelectedItemsSynchronizer.csproj new file mode 100644 index 0000000..f81fbc1 --- /dev/null +++ b/SelectedItemsSynchronizer/PrimS.SelectedItemsSynchronizer.csproj @@ -0,0 +1,75 @@ + + + + + Debug + AnyCPU + {6FD3314F-9A43-4FCF-A073-6C97CA2D1E81} + Library + Properties + PrimS.SelectedItemsSynchronizer + PrimS.SelectedItemsSynchronizer + v4.0 + 512 + + 44b8d61f + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + bin\Release\PrimS.SelectedItemsSynchronizer.XML + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + \ No newline at end of file diff --git a/SelectedItemsSynchronizer/Properties/AssemblyInfo.cs b/SelectedItemsSynchronizer/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..85d60e3 --- /dev/null +++ b/SelectedItemsSynchronizer/Properties/AssemblyInfo.cs @@ -0,0 +1,15 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("SelectedItemsSynchronizer")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("SelectedItemsSynchronizer")] +[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] +[assembly: Guid("6050c108-0753-48eb-8c19-b20516e1c3e7")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/SelectedItemsSynchronizer/TwoListSynchronizer.cs b/SelectedItemsSynchronizer/TwoListSynchronizer.cs new file mode 100644 index 0000000..bd1ea7c --- /dev/null +++ b/SelectedItemsSynchronizer/TwoListSynchronizer.cs @@ -0,0 +1,304 @@ +namespace PrimS.SelectedItemsSynchronizer +{ + using System; + using System.Collections; + using System.Collections.Specialized; + using System.Linq; + using System.Windows; + + /// + /// Keeps two lists synchronized. + /// + public class TwoListSynchronizer : IWeakEventListener + { + private static readonly IListItemConverter DefaultConverter = new DoNothingListItemConverter(); + private readonly IList masterList; + private readonly IListItemConverter masterTargetConverter; + private readonly IList targetList; + + /// + /// Initialises a new instance of the class. + /// + /// The master list. + /// The target list. + /// The master-target converter. + public TwoListSynchronizer(IList masterList, IList targetList, IListItemConverter masterTargetConverter) + { + this.masterList = masterList; + this.targetList = targetList; + this.masterTargetConverter = masterTargetConverter; + } + + /// + /// Initialises a new instance of the class. + /// + /// The master list. + /// The target list. + public TwoListSynchronizer(IList masterList, IList targetList) : this(masterList, targetList, DefaultConverter) + { + } + + private delegate void ChangeListAction(IList list, NotifyCollectionChangedEventArgs e, Converter converter); + + /// + /// Starts synchronizing the lists. + /// + public void StartSynchronizing() + { + try + { + lock (this) + { + this.ListenForChangeEvents(this.masterList); + this.ListenForChangeEvents(this.targetList); + + // Update the Target list from the Master list + this.SetListValuesFromSource(this.masterList, this.targetList, this.ConvertFromMasterToTarget); + + // In some cases the target list might have its own view on which items should included: + // so update the master list from the target list + // (This is the case with a ListBox SelectedItems collection: only items from the ItemsSource can be included in SelectedItems) + if (!this.TargetAndMasterCollectionsAreEqual()) + { + this.SetListValuesFromSource(this.targetList, this.masterList, this.ConvertFromTargetToMaster); + } + } + } + catch (Exception) + { + throw; + } + } + + /// + /// Stop synchronizing the lists. + /// + public void StopSynchronizing() + { + try + { + lock (this) + { + this.StopListeningForChangeEvents(this.masterList); + this.StopListeningForChangeEvents(this.targetList); + } + } + catch (Exception) + { + throw; + } + } + + /// + /// Receives events from the centralized event manager. + /// + /// The type of the calling this method. + /// Object that originated the event. + /// Event data. + /// + /// True if the listener handled the event. It is considered an error by the handling in WPF to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle. + /// + public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) + { + try + { + this.HandleCollectionChanged(sender as IList, e as NotifyCollectionChangedEventArgs); + + return true; + } + catch (Exception) + { + throw; + } + } + + /// + /// Listens for change events on a list. + /// + /// The list to listen to. + protected void ListenForChangeEvents(IList list) + { + if (list is INotifyCollectionChanged) + { + CollectionChangedEventManager.AddListener(list as INotifyCollectionChanged, this); + } + } + + /// + /// Stops listening for change events. + /// + /// The list to stop listening to. + protected void StopListeningForChangeEvents(IList list) + { + if (list is INotifyCollectionChanged) + { + CollectionChangedEventManager.RemoveListener(list as INotifyCollectionChanged, this); + } + } + + private void AddItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) + { + int itemCount = e.NewItems.Count; + + for (int i = 0; i < itemCount; i++) + { + int insertionPoint = e.NewStartingIndex + i; + + if (insertionPoint > list.Count) + { + list.Add(converter(e.NewItems[i])); + } + else + { + list.Insert(insertionPoint, converter(e.NewItems[i])); + } + } + } + + private object ConvertFromMasterToTarget(object masterListItem) + { + return this.masterTargetConverter == null ? masterListItem : this.masterTargetConverter.Convert(masterListItem); + } + + private object ConvertFromTargetToMaster(object targetListItem) + { + return this.masterTargetConverter == null ? targetListItem : this.masterTargetConverter.ConvertBack(targetListItem); + } + + private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + lock (this) + { + IList sourceList = sender as IList; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + this.PerformActionOnAllLists(this.AddItems, sourceList, e); + break; + case NotifyCollectionChangedAction.Move: + this.PerformActionOnAllLists(this.MoveItems, sourceList, e); + break; + case NotifyCollectionChangedAction.Remove: + this.PerformActionOnAllLists(this.RemoveItems, sourceList, e); + break; + case NotifyCollectionChangedAction.Replace: + this.PerformActionOnAllLists(this.ReplaceItems, sourceList, e); + break; + case NotifyCollectionChangedAction.Reset: + this.UpdateListsFromSource(sender as IList); + break; + default: + throw new NotImplementedException(string.Format("Unhandled enum member {0}", e.Action.ToString("f"))); + } + } + } + + private void MoveItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) + { + this.RemoveItems(list, e, converter); + this.AddItems(list, e, converter); + } + + private void PerformActionOnAllLists(ChangeListAction action, IList sourceList, NotifyCollectionChangedEventArgs collectionChangedArgs) + { + if (sourceList == this.masterList) + { + this.PerformActionOnList(this.targetList, action, collectionChangedArgs, this.ConvertFromMasterToTarget); + } + else + { + this.PerformActionOnList(this.masterList, action, collectionChangedArgs, this.ConvertFromTargetToMaster); + } + } + + private void PerformActionOnList(IList list, ChangeListAction action, NotifyCollectionChangedEventArgs collectionChangedArgs, Converter converter) + { + this.StopListeningForChangeEvents(list); + action(list, collectionChangedArgs, converter); + this.ListenForChangeEvents(list); + } + + private void RemoveItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) + { + int itemCount = e.OldItems.Count; + + // for the number of items being removed, remove the item from the Old Starting Index + // (this will cause following items to be shifted down to fill the hole). + for (int i = 0; i < itemCount; i++) + { + if (list.Count > e.OldStartingIndex) + { + list.RemoveAt(e.OldStartingIndex); + } + } + } + + private void ReplaceItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) + { + this.RemoveItems(list, e, converter); + this.AddItems(list, e, converter); + } + + private void SetListValuesFromSource(IList sourceList, IList targetList, Converter converter) + { + this.StopListeningForChangeEvents(targetList); + + targetList.Clear(); + + foreach (object o in sourceList) + { + targetList.Add(converter(o)); + } + + this.ListenForChangeEvents(targetList); + } + + private bool TargetAndMasterCollectionsAreEqual() + { + return this.masterList.Cast().SequenceEqual(this.targetList.Cast().Select(item => this.ConvertFromTargetToMaster(item))); + } + + /// + /// Makes sure that all synchronized lists have the same values as the source list. + /// + /// The source list. + private void UpdateListsFromSource(IList sourceList) + { + if (sourceList == this.masterList) + { + this.SetListValuesFromSource(this.masterList, this.targetList, this.ConvertFromMasterToTarget); + } + else + { + this.SetListValuesFromSource(this.targetList, this.masterList, this.ConvertFromTargetToMaster); + } + } + + /// + /// An implementation that does nothing in the conversions. + /// + internal class DoNothingListItemConverter : IListItemConverter + { + /// + /// Converts the specified master list item. + /// + /// The master list item. + /// The result of the conversion. + public object Convert(object masterListItem) + { + return masterListItem; + } + + /// + /// Converts the specified target list item. + /// + /// The target list item. + /// The result of the conversion. + public object ConvertBack(object targetListItem) + { + return targetListItem; + } + } + } +} \ No newline at end of file diff --git a/SelectedItemsSynchronizer/packages.config b/SelectedItemsSynchronizer/packages.config new file mode 100644 index 0000000..102a84e --- /dev/null +++ b/SelectedItemsSynchronizer/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Settings.StyleCop b/Settings.StyleCop new file mode 100644 index 0000000..4315891 --- /dev/null +++ b/Settings.StyleCop @@ -0,0 +1,48 @@ + + + False + False + + Autofac + bootstrapper + multiselector + proxied + + en-GB + + + + + + + True + + + + + False + + + + + True + + + + + True + True + + + + + + + True + + + + + + + \ No newline at end of file diff --git a/TwoListSynchronizer.cs b/TwoListSynchronizer.cs deleted file mode 100644 index ae39258..0000000 --- a/TwoListSynchronizer.cs +++ /dev/null @@ -1,276 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Specialized; -using System.Linq; -using System.Windows; - -namespace FunctionalFun.UI.Behaviours -{ - /// - /// Keeps two lists synchronized. - /// - public class TwoListSynchronizer : IWeakEventListener - { - private static readonly IListItemConverter DefaultConverter = new DoNothingListItemConverter(); - private readonly IList _masterList; - private readonly IListItemConverter _masterTargetConverter; - private readonly IList _targetList; - - - /// - /// Initializes a new instance of the class. - /// - /// The master list. - /// The target list. - /// The master-target converter. - public TwoListSynchronizer(IList masterList, IList targetList, IListItemConverter masterTargetConverter) - { - _masterList = masterList; - _targetList = targetList; - _masterTargetConverter = masterTargetConverter; - } - - /// - /// Initializes a new instance of the class. - /// - /// The master list. - /// The target list. - public TwoListSynchronizer(IList masterList, IList targetList) - : this(masterList, targetList, DefaultConverter) - { - } - - private delegate void ChangeListAction(IList list, NotifyCollectionChangedEventArgs e, Converter converter); - - /// - /// Starts synchronizing the lists. - /// - public void StartSynchronizing() - { - ListenForChangeEvents(_masterList); - ListenForChangeEvents(_targetList); - - // Update the Target list from the Master list - SetListValuesFromSource(_masterList, _targetList, ConvertFromMasterToTarget); - - // In some cases the target list might have its own view on which items should included: - // so update the master list from the target list - // (This is the case with a ListBox SelectedItems collection: only items from the ItemsSource can be included in SelectedItems) - if (!TargetAndMasterCollectionsAreEqual()) - { - SetListValuesFromSource(_targetList, _masterList, ConvertFromTargetToMaster); - } - } - - /// - /// Stop synchronizing the lists. - /// - public void StopSynchronizing() - { - StopListeningForChangeEvents(_masterList); - StopListeningForChangeEvents(_targetList); - } - - /// - /// Receives events from the centralized event manager. - /// - /// The type of the calling this method. - /// Object that originated the event. - /// Event data. - /// - /// true if the listener handled the event. It is considered an error by the handling in WPF to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle. - /// - public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) - { - HandleCollectionChanged(sender as IList, e as NotifyCollectionChangedEventArgs); - - return true; - } - - /// - /// Listens for change events on a list. - /// - /// The list to listen to. - protected void ListenForChangeEvents(IList list) - { - if (list is INotifyCollectionChanged) - { - CollectionChangedEventManager.AddListener(list as INotifyCollectionChanged, this); - } - } - - /// - /// Stops listening for change events. - /// - /// The list to stop listening to. - protected void StopListeningForChangeEvents(IList list) - { - if (list is INotifyCollectionChanged) - { - CollectionChangedEventManager.RemoveListener(list as INotifyCollectionChanged, this); - } - } - - private void AddItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) - { - int itemCount = e.NewItems.Count; - - for (int i = 0; i < itemCount; i++) - { - int insertionPoint = e.NewStartingIndex + i; - - if (insertionPoint > list.Count) - { - list.Add(converter(e.NewItems[i])); - } - else - { - list.Insert(insertionPoint, converter(e.NewItems[i])); - } - } - } - - private object ConvertFromMasterToTarget(object masterListItem) - { - return _masterTargetConverter == null ? masterListItem : _masterTargetConverter.Convert(masterListItem); - } - - private object ConvertFromTargetToMaster(object targetListItem) - { - return _masterTargetConverter == null ? targetListItem : _masterTargetConverter.ConvertBack(targetListItem); - } - - private void HandleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - IList sourceList = sender as IList; - - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - PerformActionOnAllLists(AddItems, sourceList, e); - break; - case NotifyCollectionChangedAction.Move: - PerformActionOnAllLists(MoveItems, sourceList, e); - break; - case NotifyCollectionChangedAction.Remove: - PerformActionOnAllLists(RemoveItems, sourceList, e); - break; - case NotifyCollectionChangedAction.Replace: - PerformActionOnAllLists(ReplaceItems, sourceList, e); - break; - case NotifyCollectionChangedAction.Reset: - UpdateListsFromSource(sender as IList); - break; - default: - break; - } - } - - private void MoveItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) - { - RemoveItems(list, e, converter); - AddItems(list, e, converter); - } - - private void PerformActionOnAllLists(ChangeListAction action, IList sourceList, NotifyCollectionChangedEventArgs collectionChangedArgs) - { - if (sourceList == _masterList) - { - PerformActionOnList(_targetList, action, collectionChangedArgs, ConvertFromMasterToTarget); - } - else - { - PerformActionOnList(_masterList, action, collectionChangedArgs, ConvertFromTargetToMaster); - } - } - - private void PerformActionOnList(IList list, ChangeListAction action, NotifyCollectionChangedEventArgs collectionChangedArgs, Converter converter) - { - StopListeningForChangeEvents(list); - action(list, collectionChangedArgs, converter); - ListenForChangeEvents(list); - } - - private void RemoveItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) - { - int itemCount = e.OldItems.Count; - - // for the number of items being removed, remove the item from the Old Starting Index - // (this will cause following items to be shifted down to fill the hole). - for (int i = 0; i < itemCount; i++) - { - list.RemoveAt(e.OldStartingIndex); - } - } - - private void ReplaceItems(IList list, NotifyCollectionChangedEventArgs e, Converter converter) - { - RemoveItems(list, e, converter); - AddItems(list, e, converter); - } - - private void SetListValuesFromSource(IList sourceList, IList targetList, Converter converter) - { - StopListeningForChangeEvents(targetList); - - targetList.Clear(); - - foreach (object o in sourceList) - { - targetList.Add(converter(o)); - } - - ListenForChangeEvents(targetList); - } - - private bool TargetAndMasterCollectionsAreEqual() - { - return _masterList.Cast().SequenceEqual(_targetList.Cast().Select(item => ConvertFromTargetToMaster(item))); - } - - /// - /// Makes sure that all synchronized lists have the same values as the source list. - /// - /// The source list. - private void UpdateListsFromSource(IList sourceList) - { - if (sourceList == _masterList) - { - SetListValuesFromSource(_masterList, _targetList, ConvertFromMasterToTarget); - } - else - { - SetListValuesFromSource(_targetList, _masterList, ConvertFromTargetToMaster); - } - } - - - - - /// - /// An implementation that does nothing in the conversions. - /// - internal class DoNothingListItemConverter : IListItemConverter - { - /// - /// Converts the specified master list item. - /// - /// The master list item. - /// The result of the conversion. - public object Convert(object masterListItem) - { - return masterListItem; - } - - /// - /// Converts the specified target list item. - /// - /// The target list item. - /// The result of the conversion. - public object ConvertBack(object targetListItem) - { - return targetListItem; - } - } - } -} \ No newline at end of file diff --git a/ViewModel.cs b/ViewModel.cs deleted file mode 100644 index aafcec8..0000000 --- a/ViewModel.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.ComponentModel; -using System.Collections.ObjectModel; -using System.Collections; -using System.Windows.Input; - -namespace SelectedItemsBindingDemo -{ - public class ViewModel : INotifyPropertyChanged - { - ObservableCollection _selectedNames = new ObservableCollection(); - string _summary; - - public ViewModel() - { - _selectedNames.CollectionChanged += (sender, e) => UpdateSummary(); - } - - public IEnumerable AvailableNames - { - get - { - return new string[] - { - "Abraham", "George", "James", "Joel", "John", "Peter", "Samuel", "Zachariah" - }; - } - } - - public ObservableCollection SelectedNames - { - get - { - return _selectedNames; - } - } - - public string Summary - { - get - { - return _summary; - } - private set - { - _summary = value; - OnPropertyChanged("Summary"); - } - } - - public ICommand SelectAll - { - get - { - return new RelayCommand( - parameter => - { - SelectedNames.Clear(); - foreach (var item in AvailableNames) - { - SelectedNames.Add(item); - } - }); - } - } - - protected void OnPropertyChanged(string propertyName) - { - var handler = PropertyChanged; - - if (handler != null) - { - handler(this, new PropertyChangedEventArgs(propertyName)); - } - } - - private void UpdateSummary() - { - Summary = string.Format("{0} names are selected.", SelectedNames.Count); - } - - #region INotifyPropertyChanged Members - - public event PropertyChangedEventHandler PropertyChanged; - - #endregion - } -} diff --git a/Window1.xaml b/Window1.xaml deleted file mode 100644 index 20c4e75..0000000 --- a/Window1.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - -