From 9a020b2cef498c7533bc5fa6cc0050059fcf887a Mon Sep 17 00:00:00 2001 From: James Croft Date: Wed, 23 Feb 2022 18:56:46 +0000 Subject: [PATCH 1/7] Updated project packages --- MADE-Uno.sln | 63 ++++++++++++++++++- .../MADE.Samples.Droid.csproj | 6 +- .../MADE.Samples.UWP/MADE.Samples.UWP.csproj | 8 ++- .../MADE.Samples.Wasm.csproj | 12 ++-- .../MADE.Samples.iOS/MADE.Samples.iOS.csproj | 6 +- .../MADE.UI.Controls.ChipBox.csproj | 36 +++++++++++ .../MADE.UI.Controls.DropDownList.csproj | 2 +- .../MADE.UI.Controls.FilePicker.csproj | 4 +- .../MADE.UI.Controls.Validator.csproj | 4 +- src/MADE.UI.Styling/MADE.UI.Styling.csproj | 4 +- .../MADE.UI.ViewManagement.csproj | 4 +- .../MADE.UI.Views.Dialogs.csproj | 4 +- .../MADE.UI.Views.Navigation.Mvvm.csproj | 4 +- .../MADE.UI.Views.Navigation.csproj | 4 +- src/MADE.UI/MADE.UI.csproj | 4 +- 15 files changed, 132 insertions(+), 33 deletions(-) create mode 100644 src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj diff --git a/MADE-Uno.sln b/MADE-Uno.sln index 2271bf0..fc44b96 100644 --- a/MADE-Uno.sln +++ b/MADE-Uno.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31624.102 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32126.317 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{01380FB8-F8A7-4416-AABA-5407574B7723}" EndProject @@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.UI.Styling", "src\MADE EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.UI.ViewManagement", "src\MADE.UI.ViewManagement\MADE.UI.ViewManagement.csproj", "{442D1E25-FFD1-405D-A1FC-40CAFCAD190C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MADE.UI.Controls.ChipBox", "src\MADE.UI.Controls.ChipBox\MADE.UI.Controls.ChipBox.csproj", "{D1A16208-5A34-4CC1-B175-01B5AC99E69E}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution samples\MADE.Samples\MADE.Samples.Shared\MADE.Samples.Shared.projitems*{04f1b32d-9056-43fc-b4c2-b8c5481bdacb}*SharedItemsImports = 4 @@ -799,6 +801,62 @@ Global {442D1E25-FFD1-405D-A1FC-40CAFCAD190C}.Release|x64.Build.0 = Release|Any CPU {442D1E25-FFD1-405D-A1FC-40CAFCAD190C}.Release|x86.ActiveCfg = Release|Any CPU {442D1E25-FFD1-405D-A1FC-40CAFCAD190C}.Release|x86.Build.0 = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|ARM.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|ARM64.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|ARM64.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|iPhone.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|x64.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|x64.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|x86.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.AppStore|x86.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|ARM.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|ARM.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|ARM64.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|iPhone.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|x64.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Debug|x86.Build.0 = Debug|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|Any CPU.Build.0 = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|ARM.ActiveCfg = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|ARM.Build.0 = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|ARM64.ActiveCfg = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|ARM64.Build.0 = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|iPhone.ActiveCfg = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|iPhone.Build.0 = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|x64.ActiveCfg = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|x64.Build.0 = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|x86.ActiveCfg = Release|Any CPU + {D1A16208-5A34-4CC1-B175-01B5AC99E69E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -819,6 +877,7 @@ Global {0CA60466-059C-42D3-9B68-6BBB75A75090} = {01380FB8-F8A7-4416-AABA-5407574B7723} {F8D00106-0598-45E7-B92E-EF408249C02E} = {01380FB8-F8A7-4416-AABA-5407574B7723} {442D1E25-FFD1-405D-A1FC-40CAFCAD190C} = {01380FB8-F8A7-4416-AABA-5407574B7723} + {D1A16208-5A34-4CC1-B175-01B5AC99E69E} = {01380FB8-F8A7-4416-AABA-5407574B7723} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3921AD86-E6C0-4436-8880-2D9EDFAD6151} diff --git a/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj b/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj index a6f14fe..5015b2d 100644 --- a/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj +++ b/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj @@ -78,10 +78,10 @@ 2.1.0-uno.32 - 7.1.10 + 7.1.11 - - + + diff --git a/samples/MADE.Samples/MADE.Samples.UWP/MADE.Samples.UWP.csproj b/samples/MADE.Samples/MADE.Samples.UWP/MADE.Samples.UWP.csproj index 30a9837..ee6178e 100644 --- a/samples/MADE.Samples/MADE.Samples.UWP/MADE.Samples.UWP.csproj +++ b/samples/MADE.Samples/MADE.Samples.UWP/MADE.Samples.UWP.csproj @@ -41,8 +41,8 @@ MADE.Samples en-US UAP - 10.0.19041.0 - 10.0.17763.0 + 10.0.22000.0 + 10.0.19041.0 14 512 {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} @@ -143,6 +143,10 @@ + + {d1a16208-5a34-4cc1-b175-01b5ac99e69e} + MADE.UI.Controls.ChipBox + {347cdc37-e140-42fa-8710-a0f3297d2b6b} MADE.UI.Controls.DropDownList diff --git a/samples/MADE.Samples/MADE.Samples.Wasm/MADE.Samples.Wasm.csproj b/samples/MADE.Samples/MADE.Samples.Wasm/MADE.Samples.Wasm.csproj index e165854..fe37857 100644 --- a/samples/MADE.Samples/MADE.Samples.Wasm/MADE.Samples.Wasm.csproj +++ b/samples/MADE.Samples/MADE.Samples.Wasm/MADE.Samples.Wasm.csproj @@ -53,12 +53,12 @@ - - - - - - + + + + + + diff --git a/samples/MADE.Samples/MADE.Samples.iOS/MADE.Samples.iOS.csproj b/samples/MADE.Samples/MADE.Samples.iOS/MADE.Samples.iOS.csproj index 1bce194..7297665 100644 --- a/samples/MADE.Samples/MADE.Samples.iOS/MADE.Samples.iOS.csproj +++ b/samples/MADE.Samples/MADE.Samples.iOS/MADE.Samples.iOS.csproj @@ -133,10 +133,10 @@ 2.1.0-uno.32 - 7.1.10 + 7.1.11 - - + + diff --git a/src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj b/src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj new file mode 100644 index 0000000..7dab66f --- /dev/null +++ b/src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj @@ -0,0 +1,36 @@ + + + + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + true + MADE.NET UI Chip Input Control + + This package includes UI components for Windows and Uno Platform applications such as: + - ChipBox for providing a chip input control that allows users to enter multiple values into an input. + + MADE UI Views Controls UWP Chips ChipBox TextBox + true + MADE.UI.Controls.ChipBox + + + + + + + + + + + + + Designer + MSBuild:Compile + PreserveNewest + + + + + + + + \ No newline at end of file diff --git a/src/MADE.UI.Controls.DropDownList/MADE.UI.Controls.DropDownList.csproj b/src/MADE.UI.Controls.DropDownList/MADE.UI.Controls.DropDownList.csproj index 4214734..1415751 100644 --- a/src/MADE.UI.Controls.DropDownList/MADE.UI.Controls.DropDownList.csproj +++ b/src/MADE.UI.Controls.DropDownList/MADE.UI.Controls.DropDownList.csproj @@ -1,7 +1,7 @@ - uap10.0.17763 + uap10.0.19041 true MADE.NET UI DropDownList Control diff --git a/src/MADE.UI.Controls.FilePicker/MADE.UI.Controls.FilePicker.csproj b/src/MADE.UI.Controls.FilePicker/MADE.UI.Controls.FilePicker.csproj index b7ca92c..b300c1b 100644 --- a/src/MADE.UI.Controls.FilePicker/MADE.UI.Controls.FilePicker.csproj +++ b/src/MADE.UI.Controls.FilePicker/MADE.UI.Controls.FilePicker.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI FilePicker Control @@ -14,7 +14,7 @@ - + diff --git a/src/MADE.UI.Controls.Validator/MADE.UI.Controls.Validator.csproj b/src/MADE.UI.Controls.Validator/MADE.UI.Controls.Validator.csproj index 20d64de..8df7b90 100644 --- a/src/MADE.UI.Controls.Validator/MADE.UI.Controls.Validator.csproj +++ b/src/MADE.UI.Controls.Validator/MADE.UI.Controls.Validator.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI DropDownList Control @@ -14,7 +14,7 @@ - + diff --git a/src/MADE.UI.Styling/MADE.UI.Styling.csproj b/src/MADE.UI.Styling/MADE.UI.Styling.csproj index bcb069f..6c18d4f 100644 --- a/src/MADE.UI.Styling/MADE.UI.Styling.csproj +++ b/src/MADE.UI.Styling/MADE.UI.Styling.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI Styling @@ -14,7 +14,7 @@ - + diff --git a/src/MADE.UI.ViewManagement/MADE.UI.ViewManagement.csproj b/src/MADE.UI.ViewManagement/MADE.UI.ViewManagement.csproj index f4a7485..e754c5e 100644 --- a/src/MADE.UI.ViewManagement/MADE.UI.ViewManagement.csproj +++ b/src/MADE.UI.ViewManagement/MADE.UI.ViewManagement.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI View Management @@ -14,7 +14,7 @@ - + diff --git a/src/MADE.UI.Views.Dialogs/MADE.UI.Views.Dialogs.csproj b/src/MADE.UI.Views.Dialogs/MADE.UI.Views.Dialogs.csproj index b1764f9..ee90da2 100644 --- a/src/MADE.UI.Views.Dialogs/MADE.UI.Views.Dialogs.csproj +++ b/src/MADE.UI.Views.Dialogs/MADE.UI.Views.Dialogs.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI View Dialogs @@ -13,7 +13,7 @@ - + diff --git a/src/MADE.UI.Views.Navigation.Mvvm/MADE.UI.Views.Navigation.Mvvm.csproj b/src/MADE.UI.Views.Navigation.Mvvm/MADE.UI.Views.Navigation.Mvvm.csproj index 6d7ecab..f5234a2 100644 --- a/src/MADE.UI.Views.Navigation.Mvvm/MADE.UI.Views.Navigation.Mvvm.csproj +++ b/src/MADE.UI.Views.Navigation.Mvvm/MADE.UI.Views.Navigation.Mvvm.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI View Navigation for MVVM Toolkit @@ -14,7 +14,7 @@ - + diff --git a/src/MADE.UI.Views.Navigation/MADE.UI.Views.Navigation.csproj b/src/MADE.UI.Views.Navigation/MADE.UI.Views.Navigation.csproj index 5a2304c..595b525 100644 --- a/src/MADE.UI.Views.Navigation/MADE.UI.Views.Navigation.csproj +++ b/src/MADE.UI.Views.Navigation/MADE.UI.Views.Navigation.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI View Navigation @@ -14,7 +14,7 @@ - + diff --git a/src/MADE.UI/MADE.UI.csproj b/src/MADE.UI/MADE.UI.csproj index ec83ef3..145db05 100644 --- a/src/MADE.UI/MADE.UI.csproj +++ b/src/MADE.UI/MADE.UI.csproj @@ -1,7 +1,7 @@ - uap10.0.17763;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 + uap10.0.19041;MonoAndroid10.0;xamarinios10;netstandard2.0;xamarinmac20 true MADE.NET UI @@ -15,7 +15,7 @@ - + From e01bff62cfcd84821ec91cbb69991b09ceb0a74a Mon Sep 17 00:00:00 2001 From: James Croft Date: Wed, 23 Feb 2022 20:30:39 +0000 Subject: [PATCH 2/7] Added Chip control --- .../MADE.Samples.Droid.csproj | 4 + .../Home/ViewModels/MainPageViewModel.cs | 4 + .../Samples/Assets/ChipBox/ChipBox.png | Bin 0 -> 4433 bytes .../Samples/Assets/ChipBox/ChipBoxCode.txt | 4 + .../Samples/Assets/ChipBox/ChipBoxXaml.txt | 17 ++ .../Features/Samples/Pages/ChipBoxPage.xaml | 66 ++++++++ .../Samples/Pages/ChipBoxPage.xaml.cs | 21 +++ .../ViewModels/ChipBoxPageViewModel.cs | 15 ++ .../MADE.Samples.Shared.projitems | 19 +++ .../MADE.Samples.Wasm.csproj | 1 + .../MADE.Samples.iOS/MADE.Samples.iOS.csproj | 4 + src/MADE.UI.Controls.ChipBox/Chip.cs | 79 ++++++++++ src/MADE.UI.Controls.ChipBox/ChipBox.cs | 147 ++++++++++++++++++ .../ChipBoxAutomationPeer.cs | 86 ++++++++++ .../ChipRemoveEventArgs.cs | 20 +++ .../ChipRemoveEventHandler.cs | 12 ++ src/MADE.UI.Controls.ChipBox/IChip.cs | 13 ++ src/MADE.UI.Controls.ChipBox/IChipBox.cs | 42 +++++ .../MADE.UI.Controls.ChipBox.csproj | 2 +- .../Themes/Generic.xaml | 120 ++++++++++++++ 20 files changed, 675 insertions(+), 1 deletion(-) create mode 100644 samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBox.png create mode 100644 samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxCode.txt create mode 100644 samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt create mode 100644 samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml create mode 100644 samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml.cs create mode 100644 samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs create mode 100644 src/MADE.UI.Controls.ChipBox/Chip.cs create mode 100644 src/MADE.UI.Controls.ChipBox/ChipBox.cs create mode 100644 src/MADE.UI.Controls.ChipBox/ChipBoxAutomationPeer.cs create mode 100644 src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs create mode 100644 src/MADE.UI.Controls.ChipBox/ChipRemoveEventHandler.cs create mode 100644 src/MADE.UI.Controls.ChipBox/IChip.cs create mode 100644 src/MADE.UI.Controls.ChipBox/IChipBox.cs create mode 100644 src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml diff --git a/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj b/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj index 5015b2d..9fd02b6 100644 --- a/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj +++ b/samples/MADE.Samples/MADE.Samples.Droid/MADE.Samples.Droid.csproj @@ -107,6 +107,10 @@ + + {d1a16208-5a34-4cc1-b175-01b5ac99e69e} + MADE.UI.Controls.ChipBox + {774fd8d5-ccc1-4eed-aa14-f7069bfae5ce} MADE.UI.Controls.FilePicker diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Home/ViewModels/MainPageViewModel.cs b/samples/MADE.Samples/MADE.Samples.Shared/Features/Home/ViewModels/MainPageViewModel.cs index 9fe86c7..c8b24c3 100644 --- a/samples/MADE.Samples/MADE.Samples.Shared/Features/Home/ViewModels/MainPageViewModel.cs +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Home/ViewModels/MainPageViewModel.cs @@ -32,6 +32,10 @@ private static ICollection GetSampleGroups() Name = "Controls", Samples = { + new Sample( + "ChipBox", + typeof(ChipBoxPage), + "/Features/Samples/Assets/ChipBox/ChipBox.png"), new Sample( "FilePicker", typeof(FilePickerPage), diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBox.png b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBox.png new file mode 100644 index 0000000000000000000000000000000000000000..4a57c13f0bdfd696dedec62166158f7e311c3a31 GIT binary patch literal 4433 zcmdT|d03L!{s)XS*J;u$Q-`ok4H0({vRn$TqNSOpB_g~EBLV>`E{V3gq?wkvYhvCq zw>xfGCQj*0X=Rp`p~+;+n3;KV8%_PaV2jd7p=boo+kS zly#IL5QrL);N$^;KoQ_xth5yT6;)lYgFxWEjNLwbACfDU$__Q5(Aa^1i6E2%;t&YV zUcjMHLjXP^5C~?l@W_#?%}4};hDUl^l1xb)M}W>CL~#MnD7W3zs1T|x4Qanc87IJk z20{To1tAD!vUpel9x1nr1^2RRG!h{f;fLUnKBS!pM>ZEgSejUxm?F0*BXC?=5Z1$K z`zK}agh$f(d=3_kj);gbi7+=|bA!;;9 zSiDbkfyAL@2qdFX-MyD8|<8E9%H*vbmmM83K4DMt&{h4oeKB1Owg-8lV2v+E>68Up4DdY*PUMi!R}56LumCGjRwZ(5?_XnIvVoOHNMh5#0L|rUUm>ES z<4!I+h`|Igp2rRc1ku^i3S(+4AU`ei#Wp5?;k0mMGGxtwWl~td zc%%SD1A-`FOg?gpCnc1_1c+2Ni-$wY`iIH*WJG@YYz2q@#sv-xkwn507(70k8@b4< zCvf0fuktoTAmqh`rBG#g!6P|bHZ6<_(BwRV@h_k}b`U>;!UeVkgLQ>RZVL)xfEkQL zfE~^VW&vCT*r%qSa`r_jf)2E$Z%vhlE$p-r);o znqKGL#cSiLlP~#c^%Vy5f@l_+=MwHMy_PyfZ!!L9?=BS84EufY3&GW*j%x2bw~DUV z`II;CJ03+H8*w6?AUA=lAka40=9*z|;e|8s->AQ-dn#*Q4<9?y?yg?CZ;taM+4*Pb z#TDwJ*aOeC%NU;8XERqkG>;rF)qlM{7GnmZVgCM9K87DP?+)vU4I9J6Pi4 zmT_WaE~Oxl80gibrhIPmX(TB{eb?oZDul8()@$>lV1WlrA@hi)rMVi^h} zOJgb|6EoLCh5B3nz~=%Sv&7!&QFTLu`pY7!dRI0W9xTNyrS$z0tW!^lDP#XNib*rQZ{H`tI5ccvQkd^CY z3rh4@_Z|XK{*LIhZMPtIxR0E?4_N)n=t#x|>7x>t2*>UVd*}o3qbn194GD)yh_zPL zYZDWM?|+`M_CMjJqKpZEs(H6LCA1Z4FHQ4EH&Qy5fmE6!Y8tgA!OkYuG=v(S@z9Y( zjph^mdh_yovUvHE9kKSeUUY=__eAFB4YKd%M)u=T0joKh#7o{G8w_LGijC7OhLi7{ zsMy*y20)0))c@gSZr&L8I13u1I%QL3+HjVISLua*)U?s@OMIw$1>U(aF6VsGx%0d@ zw!-mvyTc)AI&16e1H5-=cPbw4JiI%p7xsJTefU_*-urL@Da{W;%-*hwAW~$Dt5PU* z0_E;GX(KG1S2U zwzeptHf~qf$eSDDEz)~MTEv%XFRe7_wpCD@C6^W7K_`yw$!OjsJuI#jZS^HRH8{<+ zJ}c4hw2p5TTgUwXRdg(49P^I1PjiS~Z@Q+zx(0bMx2RX$p@7V^KCu`V1A9_28 zt8by=UcqMKGmBLih5JPVK=J*f1mNY$t#3{S59};n9R1h*;bO0SqPAaAaTAamBFlg` zcX3@$ulIdY9PaM*J|=gNW&FH*)s=F1G_*mvVbzpIRbI^_V)`0`JZG971Nq#y>oVOi zqNn_r;*>5&@0XNd-J7egr!%0XT8}k&%;l92m&h@L_kLZ~g+1amf$g`Rxpj>Uxrh(8 z)DrrBsm2!>y9p7x5lppq-VKIKYTak(*0U(jbeUG-^-H3cOgd}J}{GB zdp_;sJlS1$e13%O?mP(yXT5`k2Onpo{s{6Gj%;baHN?8J8KDDZ z(Vnm0w0w5>i1#v_U7p_$2zFRim7FTPSG=j*LdOPn)54a<0fGROP?wAYQ%hRindz|c@Gfp*?xSnvkS&hNy~pFq;^)s^E1zv0Drs({U0JCrjp~{i%oACQ50ILi%h2A}eWxBIBXWN3bT3=vkI7wZp0tkNgOp0e%gGZtyT)s;fKjHYVE_SAOU zi$BG))8=Y!m53&1^EM;#7B1=pPT}Jr&86|+sh z&>xYba7%E$`|6uphAYFtnQ=RC=6I*kg;$fU;lOdsaO(D)C*-R^Gi!S1e$4cP+(Fmb zIh38t4!`rkE}6j<-qZ5?0vX&XNyzW8)QV0T&aq35^sEl;@XZ@|Yk;?!ux^;`HmQf` zAP;`@4|gED`5Sci4YGYSw5Zwqn6hD|!RqQy{&zk&leH}80kg;GNx|RD|E0he7yEy| zpDybz#gKx8MyPrS7LvO?Htfs+ql@np^xAYcwRK!7IX{=Z4O^NqtHu)6wh!A+n|P3T z=T))}nI{gb=(WAwT*ou6C{gASD(CF%lB=6z&J49`?@F{H(KiwW@|(0KGQziIuV#Xz9m{o;C^5>UVY$c|JAAnl4l3Z z#Hd2jFP-qd+6$xGRb}yUKo|$r=cAyiFbPA4uc|LwWv&`ic&u?6upGqnxJD&!pb8_6 zB;;YP3L-8izF2&m-WEOl6Vbg5?6CE1{l3Zj6>_~O`)B_VH|C=Yf0Vh3EsPwjYxd29 zbr@8pzM3ON9o?(G4}yc~s+QBa#bS~us#)RVrJJD>FmvM7n)O9!t$}xGrrK@5jr8Cj(A1mlYk0HNP1?@)e!NR+EzS}Z?uTw(H$LfWpOklqB2^LZAfy|QtR0u!@&Eg5>);My z*yO$K<0Wev#aG*t@tp0?xUADRJ8Gf zaqg3Ts{nGU{XET0api$P;SDiMS|&b-pNp9(+tWSk#kjl-y2LQ%k@@IG>hVqUk4pVe z8}ijT){MzIvU|mH>2~SuW!JUu_ivDfh&CRue?qo=^QO}1G!FZZM$ z6!VwlZ>sZ)&opYRT%IysThoy2#V3bG6PiL_`lLo1H=<{Tnz1uO^=ToTDCp&8+8BZCr8qRc*0p{Q`h|X?K6%GN1{tx(-{;2=} literal 0 HcmV?d00001 diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxCode.txt b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxCode.txt new file mode 100644 index 0000000..03744c2 --- /dev/null +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxCode.txt @@ -0,0 +1,4 @@ +public ObservableCollection FilePickerFiles { get; } = + new ObservableCollection(); + +public ICollection FilePickerTypes => new List { ".jpg" }; \ No newline at end of file diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt new file mode 100644 index 0000000..f476519 --- /dev/null +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt @@ -0,0 +1,17 @@ + + + + + + diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml new file mode 100644 index 0000000..fb33223 --- /dev/null +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 9d96e2413a9479474716b2d67c8da2a2c7787eda Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 24 Feb 2022 07:42:00 +0000 Subject: [PATCH 3/7] Created UX for chip box --- .../Features/Samples/Pages/ChipBoxPage.xaml | 4 +- .../ViewModels/ChipBoxPageViewModel.cs | 35 ++ src/MADE.UI.Controls.ChipBox/Chip.cs | 6 +- src/MADE.UI.Controls.ChipBox/ChipBox.cs | 181 ++++++--- src/MADE.UI.Controls.ChipBox/ChipItem.cs | 14 + .../ChipRemoveEventArgs.cs | 5 +- src/MADE.UI.Controls.ChipBox/IChipBox.cs | 23 +- .../Themes/Generic.xaml | 31 +- src/MADE.UI.Controls.ChipBox/WrapPanel.cs | 375 ++++++++++++++++++ 9 files changed, 598 insertions(+), 76 deletions(-) create mode 100644 src/MADE.UI.Controls.ChipBox/ChipItem.cs create mode 100644 src/MADE.UI.Controls.ChipBox/WrapPanel.cs diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml index fb33223..20a8947 100644 --- a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml @@ -56,7 +56,9 @@ + Header="ChipBox" + Suggestions="{x:Bind ViewModel.ChipSuggestions}" + Chips="{x:Bind ViewModel.SelectedChips}"/> diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs index 76b2943..7042a42 100644 --- a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs @@ -1,5 +1,7 @@ namespace MADE.Samples.Features.Samples.ViewModels { + using System.Collections.Generic; + using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.Messaging; using MADE.UI.Controls; using MADE.UI.Views.Navigation; @@ -11,5 +13,38 @@ public ChipBoxPageViewModel(INavigationService navigationService, IMessenger mes : base(navigationService, messenger) { } + + public ObservableCollection SelectedChips { get; } = new ObservableCollection(); + + public ICollection ChipSuggestions => new List + { + "Austria", + "Belgium", + "Bulgaria", + "Croatia", + "Cyprus", + "Czechia", + "Denmark", + "Estonia", + "Finland", + "France", + "Germany", + "Greece", + "Hungary", + "Ireland", + "Italy", + "Latvia", + "Lithuania", + "Luxembourg", + "Malta", + "Netherlands", + "Poland", + "Portugal", + "Romania", + "Slovakia", + "Slovenia", + "Spain", + "Sweden" + }; } } \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/Chip.cs b/src/MADE.UI.Controls.ChipBox/Chip.cs index b262f39..6ea6894 100644 --- a/src/MADE.UI.Controls.ChipBox/Chip.cs +++ b/src/MADE.UI.Controls.ChipBox/Chip.cs @@ -5,6 +5,7 @@ namespace MADE.UI.Controls using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; + [TemplatePart(Name = ChipContentPart, Type = typeof(ContentPresenter))] [TemplatePart(Name = ChipRemoveButtonPart, Type = typeof(Button))] public sealed partial class Chip : ContentControl, IChip { @@ -20,6 +21,7 @@ public sealed partial class Chip : ContentControl, IChip typeof(Chip), new PropertyMetadata(true, (o, args) => ((Chip)o).SetRemoveButtonVisibility())); + private const string ChipContentPart = "ChipContent"; private const string ChipRemoveButtonPart = "ChipRemoveButton"; public Chip() @@ -67,8 +69,8 @@ protected override void OnApplyTemplate() private void OnRemoveClick(object sender, RoutedEventArgs e) { - this.RemoveCommand?.Execute(this); - this.Removed?.Invoke(this, new ChipRemoveEventArgs()); + this.RemoveCommand?.Execute(this.Content); + this.Removed?.Invoke(this, new ChipRemoveEventArgs(this.Content)); } private void SetRemoveButtonVisibility() diff --git a/src/MADE.UI.Controls.ChipBox/ChipBox.cs b/src/MADE.UI.Controls.ChipBox/ChipBox.cs index 2e12093..fb0527c 100644 --- a/src/MADE.UI.Controls.ChipBox/ChipBox.cs +++ b/src/MADE.UI.Controls.ChipBox/ChipBox.cs @@ -4,17 +4,17 @@ namespace MADE.UI.Controls { using System.Collections.Generic; - using Windows.Foundation; + using System.Collections.ObjectModel; + using System.Linq; using Windows.UI.Xaml; using Windows.UI.Xaml.Automation.Peers; using Windows.UI.Xaml.Controls; - using Windows.UI.Xaml.Controls.Primitives; - using Windows.UI.Xaml.Media; /// /// Defines a control for providing multi value input for a text box. /// [TemplatePart(Name = ChipBoxTextBoxPart, Type = typeof(AutoSuggestBox))] + [TemplatePart(Name = ChipBoxItemsViewPart, Type = typeof(ItemsControl))] public partial class ChipBox : Control, IChipBox { /// @@ -27,43 +27,45 @@ public partial class ChipBox : Control, IChipBox new PropertyMetadata(default)); /// - /// Identifies the dependency property. + /// Identifies the dependency property. /// - public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( - nameof(ItemsSource), - typeof(object), + public static readonly DependencyProperty ChipsProperty = DependencyProperty.Register( + nameof(Chips), + typeof(IList), typeof(ChipBox), - new PropertyMetadata(default)); + new PropertyMetadata(new ObservableCollection())); /// - /// Identifies the dependency property. + /// Identifies the dependency property. /// - public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register( - nameof(ItemTemplate), - typeof(DataTemplate), + public static readonly DependencyProperty ChipItemsViewStyleProperty = DependencyProperty.Register( + nameof(ChipItemsViewStyle), + typeof(Style), typeof(ChipBox), - new PropertyMetadata(default(DataTemplate))); + new PropertyMetadata(default(Style))); - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty ItemTemplateSelectorProperty = DependencyProperty.Register( - nameof(ItemTemplateSelector), - typeof(DataTemplateSelector), + public static readonly DependencyProperty SuggestionsProperty = DependencyProperty.Register( + nameof(Suggestions), + typeof(object), typeof(ChipBox), - new PropertyMetadata(default(DataTemplateSelector))); + new PropertyMetadata(default)); - /// - /// Identifies the dependency property. - /// - public static readonly DependencyProperty ItemsPanelProperty = DependencyProperty.Register( - nameof(ItemsPanel), - typeof(ItemsPanelTemplate), + public static readonly DependencyProperty SuggestionsItemTemplateProperty = DependencyProperty.Register( + nameof(SuggestionsItemTemplate), + typeof(DataTemplate), typeof(ChipBox), - new PropertyMetadata(default(ItemsPanelTemplate))); + new PropertyMetadata(default(DataTemplate))); + + public static readonly DependencyProperty AllowDuplicatesProperty = DependencyProperty.Register( + nameof(AllowDuplicates), + typeof(bool), + typeof(ChipBox), + new PropertyMetadata(default(bool))); private const string ChipBoxTextBoxPart = "ChipBoxTextBox"; + private const string ChipBoxItemsViewPart = "ChipBoxItemsView"; + /// /// Initializes a new instance of the class. /// @@ -82,42 +84,39 @@ public object Header } /// - /// Gets or sets an object source used to generate the content of the control. + /// Gets or sets an object source used to generate the chip content of the control. /// - public object ItemsSource + public IList Chips { - get => this.GetValue(ItemsSourceProperty); - set => this.SetValue(ItemsSourceProperty, value); + get => (IList)this.GetValue(ChipsProperty); + set => this.SetValue(ChipsProperty, value); } /// - /// Gets or sets the DataTemplate used to display each item. + /// Gets or sets the style of the items view. /// - public DataTemplate ItemTemplate + public Style ChipItemsViewStyle { - get => (DataTemplate)this.GetValue(ItemTemplateProperty); - set => this.SetValue(ItemTemplateProperty, value); + get => (Style)this.GetValue(ChipItemsViewStyleProperty); + set => this.SetValue(ChipItemsViewStyleProperty, value); } - /// - /// Gets or sets a reference to a custom DataTemplateSelector logic class. - /// - /// The DataTemplateSelector referenced by this property returns a template to apply to items. - /// - /// - public DataTemplateSelector ItemTemplateSelector + public object Suggestions { - get => (DataTemplateSelector)this.GetValue(ItemTemplateSelectorProperty); - set => this.SetValue(ItemTemplateSelectorProperty, value); + get => GetValue(SuggestionsProperty); + set => SetValue(SuggestionsProperty, value); } - /// - /// Gets or sets the template that defines the panel that controls the layout of items. - /// - public ItemsPanelTemplate ItemsPanel + public DataTemplate SuggestionsItemTemplate { - get => (ItemsPanelTemplate)this.GetValue(ItemsPanelProperty); - set => this.SetValue(ItemsPanelProperty, value); + get => (DataTemplate)GetValue(SuggestionsItemTemplateProperty); + set => SetValue(SuggestionsItemTemplateProperty, value); + } + + public bool AllowDuplicates + { + get => (bool)GetValue(AllowDuplicatesProperty); + set => SetValue(AllowDuplicatesProperty, value); } /// @@ -125,14 +124,27 @@ public ItemsPanelTemplate ItemsPanel /// public AutoSuggestBox TextBox { get; private set; } + public ItemsControl ChipItemsView { get; private set; } + /// /// Loads the relevant control template so that it's parts can be referenced. /// protected override void OnApplyTemplate() { + if (this.TextBox != null) + { + this.TextBox.QuerySubmitted -= OnChipSuggestionChosen; + } + base.OnApplyTemplate(); this.TextBox = this.GetChildView(ChipBoxTextBoxPart); + this.ChipItemsView = this.GetChildView(ChipBoxItemsViewPart); + + if (this.TextBox != null) + { + this.TextBox.QuerySubmitted += this.OnChipSuggestionChosen; + } } /// @@ -143,5 +155,74 @@ protected override AutomationPeer OnCreateAutomationPeer() { return new ChipBoxAutomationPeer(this); } + + private void OnChipSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + this.AddChip(args.QueryText); + if (sender != null) + { + sender.Text = string.Empty; + } + } + + private void AddChip(object content) + { + if (content == null || string.IsNullOrWhiteSpace(content.ToString())) + { + return; + } + + if (this.ValidateDuplicates(content)) + { + return; + } + + var chipItem = new ChipItem { Content = content }; + + if (this.ChipItemsView is { Items: { } }) + { + var chip = new Chip { Content = chipItem.Content }; + this.ChipItemsView.Items.Add(chip); + chip.Removed += OnChipRemoved; + } + + this.Chips?.Add(chipItem); + } + + private bool ValidateDuplicates(object content) + { + if (this.AllowDuplicates) + { + return false; + } + + // Check the actual chip view item first + if (this.ChipItemsView is { Items: { } }) + { + var existingChip = this.ChipItemsView.Items.Cast().FirstOrDefault(x => x.Content != null && x.Content.Equals(content)); + if (existingChip != null) + { + return true; + } + } + + var existingChipItem = this.Chips?.FirstOrDefault(x => x.Content.Equals(content)); + return existingChipItem != null; + } + + private void OnChipRemoved(object sender, ChipRemoveEventArgs args) + { + var chipItem = this.Chips?.FirstOrDefault(x => x.Content.Equals(args.Item)); + if (chipItem != null) + { + this.Chips.Remove(chipItem); + } + + if (sender is Chip chip) + { + chip.Removed -= this.OnChipRemoved; + this.ChipItemsView?.Items?.Remove(chip); + } + } } } \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/ChipItem.cs b/src/MADE.UI.Controls.ChipBox/ChipItem.cs new file mode 100644 index 0000000..c3c41de --- /dev/null +++ b/src/MADE.UI.Controls.ChipBox/ChipItem.cs @@ -0,0 +1,14 @@ +namespace MADE.UI.Controls +{ + public class ChipItem + { + public object Content { get; set; } + + /// Returns a string that represents the current object. + /// A string that represents the current object. + public override string ToString() + { + return this.Content?.ToString(); + } + } +} diff --git a/src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs b/src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs index 84f81d0..37f5924 100644 --- a/src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs +++ b/src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs @@ -13,8 +13,11 @@ public class ChipRemoveEventArgs : RoutedEventArgs /// /// Initializes a new instance of the class. /// - public ChipRemoveEventArgs() + public ChipRemoveEventArgs(object item) { + this.Item = item; } + + public object Item { get; } } } \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/IChipBox.cs b/src/MADE.UI.Controls.ChipBox/IChipBox.cs index e854cf0..5aa8e18 100644 --- a/src/MADE.UI.Controls.ChipBox/IChipBox.cs +++ b/src/MADE.UI.Controls.ChipBox/IChipBox.cs @@ -3,6 +3,8 @@ namespace MADE.UI.Controls { + using System.Collections; + using System.Collections.Generic; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; @@ -17,26 +19,17 @@ public interface IChipBox object Header { get; set; } /// - /// Gets or sets an object source used to generate the content of the control. + /// Gets or sets an object source used to generate the chip content of the control. /// - object ItemsSource { get; set; } + IList Chips { get; set; } /// - /// Gets or sets the DataTemplate used to display each item. + /// Gets or sets the style of the items view. /// - DataTemplate ItemTemplate { get; set; } + Style ChipItemsViewStyle { get; set; } - /// - /// Gets or sets a reference to a custom DataTemplateSelector logic class. - /// - /// The DataTemplateSelector referenced by this property returns a template to apply to items. - /// - /// - DataTemplateSelector ItemTemplateSelector { get; set; } + object Suggestions { get; set; } - /// - /// Gets or sets the template that defines the panel that controls the layout of items. - /// - ItemsPanelTemplate ItemsPanel { get; set; } + DataTemplate SuggestionsItemTemplate { get; set; } } } diff --git a/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml b/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml index 0e155ae..48c0ec7 100644 --- a/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml +++ b/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml @@ -10,7 +10,7 @@ - + + diff --git a/src/MADE.UI.Controls.ChipBox/WrapPanel.cs b/src/MADE.UI.Controls.ChipBox/WrapPanel.cs new file mode 100644 index 0000000..68c9c0d --- /dev/null +++ b/src/MADE.UI.Controls.ChipBox/WrapPanel.cs @@ -0,0 +1,375 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the Windows Community Toolkit project root for more information (https://github.com/CommunityToolkit/WindowsCommunityToolkit/blob/main/License.md). + +namespace MADE.UI.Controls +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Windows.Foundation; + using Windows.UI.Xaml; + using Windows.UI.Xaml.Controls; + + /// + /// WrapPanel is a panel that position child control vertically or horizontally based on the orientation and when max width / max height is reached a new row (in case of horizontal) or column (in case of vertical) is created to fit new controls. + /// + internal partial class WrapPanel : Panel + { + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HorizontalSpacingProperty = + DependencyProperty.Register( + nameof(HorizontalSpacing), + typeof(double), + typeof(WrapPanel), + new PropertyMetadata(0d, LayoutPropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty VerticalSpacingProperty = + DependencyProperty.Register( + nameof(VerticalSpacing), + typeof(double), + typeof(WrapPanel), + new PropertyMetadata(0d, LayoutPropertyChanged)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty OrientationProperty = + DependencyProperty.Register( + nameof(Orientation), + typeof(Orientation), + typeof(WrapPanel), + new PropertyMetadata(Orientation.Horizontal, LayoutPropertyChanged)); + + /// + /// Identifies the Padding dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty PaddingProperty = + DependencyProperty.Register( + nameof(Padding), + typeof(Thickness), + typeof(WrapPanel), + new PropertyMetadata(default(Thickness), LayoutPropertyChanged)); + + /// + /// Identifies the dependency property. + /// + /// The identifier for the dependency property. + public static readonly DependencyProperty StretchChildProperty = + DependencyProperty.Register( + nameof(StretchChild), + typeof(StretchChild), + typeof(WrapPanel), + new PropertyMetadata(StretchChild.None, LayoutPropertyChanged)); + + private readonly List rows = new(); + + /// + /// Gets or sets a uniform Horizontal distance (in pixels) between items when is set to Horizontal, + /// or between columns of items when is set to Vertical. + /// + public double HorizontalSpacing + { + get => (double)this.GetValue(HorizontalSpacingProperty); + set => this.SetValue(HorizontalSpacingProperty, value); + } + + /// + /// Gets or sets a uniform Vertical distance (in pixels) between items when is set to Vertical, + /// or between rows of items when is set to Horizontal. + /// + public double VerticalSpacing + { + get => (double)this.GetValue(VerticalSpacingProperty); + set => this.SetValue(VerticalSpacingProperty, value); + } + + /// + /// Gets or sets the orientation of the WrapPanel. + /// Horizontal means that child controls will be added horizontally until the width of the panel is reached, then a new row is added to add new child controls. + /// Vertical means that children will be added vertically until the height of the panel is reached, then a new column is added. + /// + public Orientation Orientation + { + get => (Orientation)this.GetValue(OrientationProperty); + set => this.SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets the distance between the border and its child object. + /// + /// + /// The dimensions of the space between the border and its child as a Thickness value. + /// Thickness is a structure that stores dimension values using pixel measures. + /// + public Thickness Padding + { + get => (Thickness)this.GetValue(PaddingProperty); + set => this.SetValue(PaddingProperty, value); + } + + /// + /// Gets or sets a value indicating how to arrange child items + /// + public StretchChild StretchChild + { + get => (StretchChild)this.GetValue(StretchChildProperty); + set => this.SetValue(StretchChildProperty, value); + } + + private static void LayoutPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is WrapPanel wp) + { + wp.InvalidateMeasure(); + wp.InvalidateArrange(); + } + } + + /// + protected override Size MeasureOverride(Size availableSize) + { + var childAvailableSize = new Size( + availableSize.Width - this.Padding.Left - this.Padding.Right, + availableSize.Height - this.Padding.Top - this.Padding.Bottom); + foreach (var child in this.Children) + { + child.Measure(childAvailableSize); + } + + var requiredSize = this.UpdateRows(availableSize); + return requiredSize; + } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + if ((this.Orientation == Orientation.Horizontal && finalSize.Width < this.DesiredSize.Width) || + (this.Orientation == Orientation.Vertical && finalSize.Height < this.DesiredSize.Height)) + { + // We haven't received our desired size. We need to refresh the rows. + this.UpdateRows(finalSize); + } + + if (this.rows.Count > 0) + { + // Now that we have all the data, we do the actual arrange pass + var childIndex = 0; + foreach (var row in this.rows) + { + foreach (var rect in row.ChildrenRects) + { + var child = this.Children[childIndex++]; + while (child.Visibility == Visibility.Collapsed) + { + // Collapsed children are not added into the rows, + // we skip them. + child = this.Children[childIndex++]; + } + + var arrangeRect = new UvRect + { + Position = rect.Position, + Size = new UvMeasure { U = rect.Size.U, V = row.Size.V }, + }; + + var finalRect = arrangeRect.ToRect(this.Orientation); + child.Arrange(finalRect); + } + } + } + + return finalSize; + } + + private Size UpdateRows(Size availableSize) + { + this.rows.Clear(); + + var paddingStart = new UvMeasure(this.Orientation, this.Padding.Left, this.Padding.Top); + var paddingEnd = new UvMeasure(this.Orientation, this.Padding.Right, this.Padding.Bottom); + + if (this.Children.Count == 0) + { + var emptySize = paddingStart.Add(paddingEnd).ToSize(this.Orientation); + return emptySize; + } + + var parentMeasure = new UvMeasure(this.Orientation, availableSize.Width, availableSize.Height); + var spacingMeasure = new UvMeasure(this.Orientation, this.HorizontalSpacing, this.VerticalSpacing); + var position = new UvMeasure(this.Orientation, this.Padding.Left, this.Padding.Top); + + var currentRow = new Row(new List(), default); + var finalMeasure = new UvMeasure(this.Orientation, width: 0.0, height: 0.0); + void Arrange(UIElement child, bool isLast = false) + { + if (child.Visibility == Visibility.Collapsed) + { + return; // if an item is collapsed, avoid adding the spacing + } + + var desiredMeasure = new UvMeasure(this.Orientation, child.DesiredSize); + if ((desiredMeasure.U + position.U + paddingEnd.U) > parentMeasure.U) + { + // next row! + position.U = paddingStart.U; + position.V += currentRow.Size.V + spacingMeasure.V; + + this.rows.Add(currentRow); + currentRow = new Row(new List(), default); + } + + // Stretch the last item to fill the available space + if (isLast) + { + desiredMeasure.U = parentMeasure.U - position.U; + } + + currentRow.Add(position, desiredMeasure); + + // adjust the location for the next items + position.U += desiredMeasure.U + spacingMeasure.U; + finalMeasure.U = Math.Max(finalMeasure.U, position.U); + } + + var lastIndex = this.Children.Count - 1; + for (var i = 0; i < lastIndex; i++) + { + Arrange(this.Children[i]); + } + + Arrange(this.Children[lastIndex], this.StretchChild == StretchChild.Last); + if (currentRow.ChildrenRects.Count > 0) + { + this.rows.Add(currentRow); + } + + if (this.rows.Count == 0) + { + var emptySize = paddingStart.Add(paddingEnd).ToSize(this.Orientation); + return emptySize; + } + + // Get max V here before computing final rect + var lastRowRect = this.rows.Last().Rect; + finalMeasure.V = lastRowRect.Position.V + lastRowRect.Size.V; + var finalRect = finalMeasure.Add(paddingEnd).ToSize(this.Orientation); + return finalRect; + } + + [System.Diagnostics.DebuggerDisplay("U = {U} V = {V}")] + private struct UvMeasure + { + internal static UvMeasure Zero => default; + + internal double U { get; set; } + + internal double V { get; set; } + + public UvMeasure(Orientation orientation, Size size) + : this(orientation, size.Width, size.Height) + { + } + + public UvMeasure(Orientation orientation, double width, double height) + { + if (orientation == Orientation.Horizontal) + { + this.U = width; + this.V = height; + } + else + { + this.U = height; + this.V = width; + } + } + + public UvMeasure Add(double u, double v) + { + return new UvMeasure { U = this.U + u, V = this.V + v }; + } + + public UvMeasure Add(UvMeasure measure) + { + return this.Add(measure.U, measure.V); + } + + public Size ToSize(Orientation orientation) + { + return orientation == Orientation.Horizontal ? new Size(this.U, this.V) : new Size(this.V, this.U); + } + } + + private struct UvRect + { + public UvMeasure Position { get; set; } + + public UvMeasure Size { get; set; } + + public Rect ToRect(Orientation orientation) + { + return orientation switch + { + Orientation.Vertical => new Rect(this.Position.V, this.Position.U, this.Size.V, this.Size.U), + Orientation.Horizontal => new Rect(this.Position.U, this.Position.V, this.Size.U, this.Size.V), + _ => ThrowArgumentException() + }; + } + + private static Rect ThrowArgumentException() + { + throw new ArgumentException("The input orientation is not valid."); + } + } + + private struct Row + { + public Row(List childrenRects, UvMeasure size) + { + this.ChildrenRects = childrenRects; + this.Size = size; + } + + public List ChildrenRects { get; } + + public UvMeasure Size { get; set; } + + public UvRect Rect => this.ChildrenRects.Count > 0 ? + new UvRect { Position = this.ChildrenRects[0].Position, Size = this.Size } : + new UvRect { Position = UvMeasure.Zero, Size = this.Size }; + + public void Add(UvMeasure position, UvMeasure size) + { + this.ChildrenRects.Add(new UvRect { Position = position, Size = size }); + this.Size = new UvMeasure + { + U = position.U + size.U, + V = Math.Max(this.Size.V, size.V), + }; + } + } + } + + /// + /// Options for how to calculate the layout of items. + /// + internal enum StretchChild + { + /// + /// Don't apply any additional stretching logic + /// + None, + + /// + /// Make the last child stretch to fill the available space + /// + Last + } +} \ No newline at end of file From e5d7844abc9e2ecb18012cff81c1422bdc0593bd Mon Sep 17 00:00:00 2001 From: James Croft Date: Thu, 24 Feb 2022 12:10:38 +0000 Subject: [PATCH 4/7] Finished up work for ChipBox --- assets/SampleIcons.afdesign | Bin 11392 -> 21610 bytes .../Samples/Assets/ChipBox/ChipBox.png | Bin 4433 -> 5663 bytes .../Samples/Assets/ChipBox/ChipBoxCode.txt | 46 ++- .../Samples/Assets/ChipBox/ChipBoxXaml.txt | 5 +- .../Features/Samples/Pages/ChipBoxPage.xaml | 16 +- .../ViewModels/ChipBoxPageViewModel.cs | 40 +- src/MADE.UI.Controls.ChipBox/Chip.cs | 31 +- src/MADE.UI.Controls.ChipBox/ChipBox.cs | 357 ++++++++++++++++-- .../ChipBoxAutomationPeer.cs | 18 +- .../ChipBoxChipRemovedEventArgs.cs | 26 ++ .../ChipBoxChipRemovedEventHandler.cs | 12 + .../ChipBoxTextChangedEventArgs.cs | 27 ++ .../ChipBoxTextChangedEventHandler.cs | 12 + src/MADE.UI.Controls.ChipBox/ChipItem.cs | 18 + ...veEventArgs.cs => ChipRemovedEventArgs.cs} | 13 +- ...tHandler.cs => ChipRemovedEventHandler.cs} | 2 +- src/MADE.UI.Controls.ChipBox/IChip.cs | 17 +- src/MADE.UI.Controls.ChipBox/IChipBox.cs | 56 ++- .../MADE.UI.Controls.ChipBox.csproj | 3 +- .../Themes/Generic.xaml | 18 +- 20 files changed, 642 insertions(+), 75 deletions(-) create mode 100644 src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventArgs.cs create mode 100644 src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventHandler.cs create mode 100644 src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventArgs.cs create mode 100644 src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventHandler.cs rename src/MADE.UI.Controls.ChipBox/{ChipRemoveEventArgs.cs => ChipRemovedEventArgs.cs} (58%) rename src/MADE.UI.Controls.ChipBox/{ChipRemoveEventHandler.cs => ChipRemovedEventHandler.cs} (81%) diff --git a/assets/SampleIcons.afdesign b/assets/SampleIcons.afdesign index ba3d264cec73ad3218d13a8ec8474b6967fdbab1..601622e61f110ead629854ec272f8cb24a2b8823 100644 GIT binary patch literal 21610 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 literal 11392 zcmdsdWl&tt()R)zvS@G*K^E8G5Zo7c2_6fcXR*u`S^ajPt~obtLDs1&vc(TJyYGknKK|jQ-J^k0lE8l>#@Sz9e0>O{~|}` zfAXw<*Z--3Kz4zGc7MxMnBq*5atdBJtP;61oq=wa;PlW0aeupPFL7IUZ} z5u^g7lEPweYR0vV{d|B$9~U)x*OB*FRiC15R2;v}i!jKV6HXVzI@*8f%(aaFK;{*+ zvxDcazCQWq989665!A|1myrI~q(ThTPgC#Ee5iusrGj^pkDeIn(>a@`H>AAx-cL+ex(Cxg} z{EsccY~gpJ=z?!96zCF^GmTg@9h@0d4zAV5aFjlbl1H*5O}QhL$sr>6UbLSs2S04+ z*gO0W;#KbyE*xB@~N)=*r<$(1)@Z(8g+bLlYSpwLiWc z6gbt$GdLC68eEE2xJBTxdHWWXJOy%Gb_M{BP~~T1U;<_{1ybD$6p?rh+gT(D3@L~+ zI7r67AR4lSuSdbfatGTJK7Jmv>Vz*hx)$1FfZ|cgAEkFNSPLpJ;EZV3(|FBnMao~n zDplZl8e@y=wMFj-Rfiv|e-J$FL4y6^Tt;L{q$p7k@l(%Gjiwcls_nQ@nAS%wxDE504 zbV_8YOxT{X)~>d2kP3teMXo~=LM@_yM^EmQiEy7#hchf1l*S&f`G6`Ns*mE))I*Ug zG5E6_^Ry+y@$JUNzmUw4Y}QhQH-xTa2P?E_4RC8lvSPBc~s&*aj*|V=!JYiIE5go{wgz+Se}8dSqMmQ zsrz|Xm3`uabxt(}W*hMa+}lM};O>eWH&j*FF#QeSW76<89sOvcuL=N^-UMsT(@1OLhbY#VBC@!maf{vG!5pRrd+1zCAGZ@=O{3-=z|hQXJD=xTWy0X? z_Fck*xKZh`=mkRB5GBhuPB}PM@M~kgozN?x^A$QSB+%k)jhp&S3sc9Nn~I;D==OWs zzkdE16gk@kWPJNv&_XZlZ*yON9&a_}CM~j9CZ7+CG$g;l&#m6)kuI8-Yk0W-@{1A` zpL&nrozY|L6Ym9vmvkTR#((7`pIa+U-LlFwzg5(l>x_S`8eW zSlRXULd*|+Z*+0)oB(pAk#^r0WF67uQNH&(Z{_T!PY=@h*b7>=yveqQa|C%RfVX2D zgztg@-Cs}nM(lXRW)S-kcBXocbG<5kw^xYTWG>P;;I>^~XZm6huCejU7EGqSWENbY z$F-IqUx2*3qPJy7R=k$3ywVSiDge~0oVjmEAEgn*<5MK-pe!l5F*<%)3sGej0pbL| zReC;{@5OP3Q+@N^TXP8_4Y%`*PVZ}qljs_+`n;&BrBlquIZZIPbeE5%@m=k^bLhK_3QJ9ytrIGUZCzSD-Hfr;3%&32N?Hkj^d@e zfLq~r#}$BSXp;TTtXI#5?j{mu~S#>Ka z57@uaKdBM3pGwU9ygs(=Dlz^$|M(B#}|kx`Dk`sVJQFPSB5E#4qPl zzD&b86B&gb%^8=Yd`HF0t3@;K3rS1J>PWzgGDujer|?sR-C~MIZ4HnPp=KYb7-Zme z#Ke42Rk#c?CciXy#{CSLAEu>C9$m~5$(ymGJrS!k1g2vd@a2Q5^T3wMp_9G$_Uylw zXJFtM;2YmmS>Lm#$n3fjw{1krUsw!EUhv;iNZ*>tlRHa95$yozQtEZ+qbM*03c zZwItI8TWA#Ml!B)=E^&rb1Z~ypyHDfW0R;6KQKLLcD`#_hjTS3CN;ydP0Ybx|d1=5UL#TPo7bIBYGue_)u92~>v4PGnpL8P#4j7nks=0V5sT-iv5Cl@>kfn}xz5 zix+UMS$D=AUx`obOPk>0AImrSILcWVd4EoKK#01$MLvgR^(?O<3vz*u?cMrPcan)EF*&YktOh*jAvz+& zCRZUp))+z=a&;sBB0X>vYYZ6olWHiQdR{X`9;9*{-xJJwtC$R@IVGAiq#b!(;c%}P-qCnj0VfkC2N0E;p=HrS>1vaCU z4n)-`lgERI2g=?g22}dtkUUk<2aF&W`MbOlSz4y)N z$A>-p@C(f5VolHAinJk?(Sw2gpg*GF+)7W#MnF9fwoa%;TW4)JyY0=ALzF_E0nNZi zU2sZkGJ~4#4aR($dRyttxRHXgA=8X zx>?Ltgit6V4#_P+5S1OVHq92yw|({z8g11;K)pr@kQ9zywasPnwJ0$?% z-kx9nng---=;I=;uOq)?10LaMxsU7Ek(%M&U-|gre$X38G%3JiVktSNNT#V)74Mwe zM3FdBC1^kFKL=>b=VgI|f!_pZPKIw@3@U8Vq+7N>|cI$078$Qg)+Vy z3zWV~59-rnnnQR6=_A4vX3~-TMx!Qgo-fhhJZBuD?6Ad(G$f*o6;#0q zslz%mxwyi{Rn6Y5!lBa3hZtsXQW)QbkLIaJ;o!!D6v-VWB18MP33PdnaG@a~SRA~b z{#B;7p+3kkvfz&GPV3M}Mjb(>8fauVJ(+}6NI#N-*j;@o0+&rAjZ^=_nh`FmYCO%u zCu`8C8CS7)-*s#w6!Y~seX+x-S|O2`;;W}&q<_0quTX&h>Do&wo&0-tCj0Z3KLC{o z$9oYLE{?t@Jg?qv@-W~hcXcS?B*8kH24sg#>+1K=w1P&@OD;xIFcO}K+|OUoas*fK ziL>x(lkpSa0$1r-wqtOhg`Ft6y0>w-)D3cB=*rd7Sg?NC#uhZV|4E`oHQnLlg0m}{ zk|WL=u(GE=X}7AVz6B^nwv_MWg!QfN+@J%RTBVLYF}h%$-)58yQ_dhDI2 zZpaPRTjsrz!=RM2Npw5-w%yp{d*L!#)==~oK_=o?v~vzb@HkYQUjwVZF2`F>+I78f z*X(_at5b_M5pQh{kzR$}a>!$0dM)d=!lwZCE>QH%T1A`K7d)JqM$@LQ#G|<~; zNw@DY!JT>ukk&m-M+H_6{dUZ+5OAlOTfDr}a|W<%-5|Y!Jf=y=0lA?-D`@@H3Bcpb zcYAX{`F7474Fnq3knelA#^nGH!qAHsTJ|D&M8`1Juw%eEmP)P4x{fXZkh8bUyJ+&* z`Lad6-%?bTP(GtR;$S|;Q{C^s68PlqWxMbg^+qiQvm1#iJ-=yBnMnV&DR<9_Vy&h5 zG7NecB8-1Ceu4N|t4LPUD?nsQ*E^7v(3mQ^V6wgQWMYO}llFyCb9JU`ih}J%hTFr* zO(SDT96-QviVNof2LoRsV~yzgh5B;mQA%~(Dr=+~ei8+v$n$SnlSPM~rWRW1rLIIW zK})YDXnfxcNYgUNSeqEl=TE|=M9z$Rje=Yb5;Nk}KK+1Q`UY}ghF@=_ZTO@n?d@cd zlAHZ>#^)c~zETpNr?2c?`L{4zhP}_2AT<6f#y8wk z1NiHRGE4St$;2G)d8?A^7f@{sC19q@Rj03R^BmZWtr4-AdAxgG8O>L-Ul34XRc1nI z{_XabZe$YRKl|-!v3RcukSS}ke{t(!(C+>HOlH>TOqk5!U1?N4G0Dp^mhBwvM2K zG@)FJ#|$V%$Jld%-9lkGeq6 zj{!ERE~T#F7D!R~xP#eFzBu3|))nH;Qz6XuY`Cj*a^ERF?!t>Fi|5ZGcq{!ncCnf4 z2w((h6Pz!q)DbmRMgol>cM_}+5agjUiMT5W1Yr(_bls(&$05ON`5$DhDgi$WRPSkr zzm^@SUq00@q!opa%FkpEEx}I#AHn^0{i{#xo3=OYx?xMMA|#H$-P$4mkj;3x1#J2Y z?sXj5PAcGjD^McxA$A++WcUaGjtp0TM^|TH_%?1WQ4tM0{Z+c3oPzAZ#AQQUmpkEZ zMpnc8JBRdUwA`nyIPj;PIG`PmN@P+{iAZQXBn)>NMQbJ@dP=`W)fBNvJf0mL#6^%dft z#gYQSts%J%m9wp#Pb95Lvi=68kdO7?IcFxdL2QB2o;2|U>5>&N}H zGmH4791qCh-vZ?Qr9<=Z*{d)K<3)6H;L0hGKMGh#7+cZp%jryhOH}n}!!tAPs7X;Rqlx`8Bf|3EW6)>wfJynmj!W%#_{&;0R0T|E>I=WaO6fHs z-fD(5+#$#&h|hM~$jeO2uy+~l7@G1M5Y9KZUAwy71iki|xw6d#JW@(0H<^pu zchHDhQ46RCxl?UGS+)7>?SVP{GZ~=8VRm{0n9Xo&xE!f(VBX)5qpmHf|E@W7?cLP= zP*dz0!B01JxEo-fJ;8bj0Oy`so@Jj)ZOSaAf9vrBKrL^1w~GTmDWHqa%@$$VCILb_ zutKkaxSgGdyO?k}C;m;_k^WK1PS{tp&(KL*5L&p?A*(dlf>!|#Cf`MhbROG`X9Dy8 zFggopj&BC2^AS(If5wdd>kN;?|F4CC^reW=%%ai<=jVeCL%@Rww4A-G1ONB3zPwrr zVDWQa{b~2@tvB-tfRhhQF&R%_f=qDp+Btw26?kiTeP#L3c;3RH!R6&sG9}|-AgTBr z#(~F|RBAibYPU?fZm=x(d^5)FiTMstO;Gl41XV;=$eLvV1Qv}@5G6ZxbUTv zep`IWK6g3iul>@urkbx;rkd*o$dW;TjAO5 zfVn5XA>pxC1*0e6(ga8?p*KUW5jcs$z2|#?tg~zE<+T6~@u!}fTLBBL8-Ve25$=|@ zjb0S}weTG=yzcKOj={+S^evYo%_g-kYX=q>0K2&|m8k>AFYgxt%+Zp4{KJjIGEYP* zFGui=+`Sj0>i(|w!v}H%jUUP}9+LMbWzDv@j{Qao=5R&C@fwkZ*8Ti-5y<6n-aEOk zmhJm_e1@O~{#gBC+yU@A4*-rmaYx1N01x(#>a&v(kNb}+$YZSK zDHM`Us^3ILoZLH~_c*C(dm`T;Rh2*k#v&oXW+DMGjdoJl5@b*|;hVmtfz3z{?L>!b zR*)5B^HhVPLzt@anMuwaaK8oUE%%vNVWkxu#(zXV?4;C-JoXm$aIs74k+)om&?o z@lkEO077w6CH9u~_4!F&vSwv#XzjVgCg1RFi#GP%CZjf!Nr99tqu4G^)vVLJ5c+(M zJaQ3xK881XF+Io6yS7_nKFe#XM+AsUXwN9(pwF%Au^-ZY{ zd;Pr(f|DX;~_} z0?Ae6?n=)-j;rv>L zgl7kSC@i&GU^^%5sK2$doo)8pvgOdsZb)_Bu-X>0@Y5{Q3BPxWj?v--wD(K%6&*aC z*?tj`l(s<3;kN7ND#L*~D>i8Ar(6WjPPy8jkAF@3Aa*A^JGrm@u*)jw_=)qkzLElk zAKh4KxTg*2MoXDvkXKt)b1{0EuY`b6rzNu;yl!sG5-^>JR*$yczL5s z2wOB&Ng_YY+VGQ?acIlg&X8Gw&#-2r0}c_(t3c`>0a>3Y*YfsQgn#nlctlVqDc)mq zj)MD}dL;+%J85v9F?0+27rcz@Sqxc5LlbsqfH}ziB$r~K*r7qj#7GCj>&M?h;|nW& zTFG!#uQvhjrl1|SjMW+Jk4IZnl5^OfxQ<+>@NH$a#iNPhlOlf-!au_T9P{?v-ff-@ z#s2P}HWdg8qRV$KdLi0bDy;g2lJv3pvc;u|$NTKj*N@4=AVpinZpjtSR>U#rh{V?n&gkV8hWYVw+rJ zCP6}ld$6YU+EZ6G)mM67MS7^XC%`F68DjgfT(p)(GV%?FIl?_zC1&L3*B$TEU%e!& zN>8nzs^mAR-hX-T8Dlnz4xtZ5x>?KEWpXC1q3YVl!KgZE885+)4evn=t_;s!87`xx z)l`HM1h~TB<$p^1L3i35uqDc07|Na$AFAm&fr=;Q3$H$<^$d+*X>cWbO%Fynjyf=L z>@~yXXwNnKs^~5l-~DmNd)(a5P4Y&ISZ(1{R8TsAn^lGagy3ZL?(Z<|qxKWwiCAlUw_IPui3aQtbQos_NHHw->t!Bt>CA$*iJS=jjV;%-aH#F2dWYhQk%J zbVaaAws;zV5>n6pOX|cArC<%ycuq)xu^O?I3-3t~X%r`SI%k>hYmY9Za1u7L1nqs{ z$cvXQ;QF`uY~VlANKcQH6)Wb-2GfVdRXe(_&=l}f6bV5!3$Z`bOm*bY===|&lo}0e zTkZUclzb!RME(+JKUeU+w=dqckF!ohi>S&z)lRHDjt8LpE(LHG7;6M#VX|J`znkI^5ar=v!MPlqXjh+yi<1|ZO1VJu?^E+)(TVakfh@I2MceLx`M zp1%nzorjnnqo@+5tYGvidvA6y(bAMQYPmA|Y2$37k1XdJ6AP|^4)UjZt=`Gw03DNFb5x_v988H+m7AP+s1PW2a z3>_b13{EHrl$ZQpz^HpKBcHMg6;Ea89l8HvTLg6uV{{i^x?4qZe|Qh-!Qsb`s!eqY zW%J8en}s4YkGI#%mj*~M8w*y{Ak3Xj5>_+g-toi2!P2Vb{n|dJQG*oIn7wuxZvmPT z;KJ!| z$-b8+uyhdM@Z%c3SuVK{mjCZD)_-);e$U`kaeA;_dLgNib6Jn@@4VMO*5Q|Zb^yJ+ zZ1wuH6O3$u$!Z*<=Jd_*u{!u~MNpNEl^#0pBiCErtYunSqNKHt;R_N0XD-NlsW)QS zA@@l^yW>2t1W##1Bn0$lnHLofMQC|D9rN-|c_H5-=cdts zJF;>kp_aOm!%q(NeI~q08apIt8lHWNNLw56QM2oGcaVPNk0x2li4R!UJQiHd%u4;+ z3r$G!-?jArQ?ohh{|DsRJE!H1T;u#IS39qMJn%;pCI&M(>6?i&6_S0n$??pC-4F41 zt|Tz_v|+z|am-7}(6Qw0?!b_yi?qy@)@=C=vmWX_u*+F{h3R+nv=3b_#Ip1q+MXpf zh7L@vzQ|PtF+^B^JSEj=;{d6_N3#*M!>b!m)~z>GUO=NIR>+|F=1Tl7?`qBZ%+DDA z9LG7NXm*)dXjpm+0GYMkbprg!$*O#Hz_eSH502n>?W(eCTK)OpjmWl1TXDveSB z2d1nYUywS1>6@G z)xERU##+5fF=%R3T38E|vs{F_~!zV&H%GnQvs z+-uN*!|usAqOt?~^8d6uR4;>az=B(YtdMV0TkC=VL3@DlJN@g(0GU=3Ij@75||)|$tG$ltKU zBAvQ`DQ_>a2Gq11R>#-RLW(E9p}9_h-)uR+pqA4lC}Kdu(0u2F?YCv1$8aSm8}nAO zT((QW)knCGm_gHp7!S*iB7nJTzjv@%&_160bK3|*BiG*ft?I1=_ssxmshhS6e^sGg zSk&A@-gsZ5Oeou3hpN#o zauKV8I)0e5HRr5R!^HmbUDLfpuU!8yxy@mR4EiOnxg)>P614X|K#RYN?*;6T0;$qj zk96*tf|k7Z*)F9~C$x>AkogNSlqoH92Nnd>l#Os?$V*(g#%VbXLLMOJa%I#B?g}<# zUwegMQ*{VqF4|)7r#v$TKt_T=APTbo62@Z>8&gsoP?hhL(Buu`Zif`=JM^77ITk3O ralq^(_j9WBduc+Dr-Uw28cjX*YYvO2|1undXF)I(9pwr|+pzx!Hd!?l diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBox.png b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBox.png index 4a57c13f0bdfd696dedec62166158f7e311c3a31..cd7413091834a7712082b9c5a71f88fbf271feae 100644 GIT binary patch delta 4366 zcmc(iXHe7Kw#E}eN9k1rLPSw%0@AAp(h(a13C%)>fK-9-4^pH^mm-KD)j|^`!2l9L ziUJ}E1gVLLh#?6*Kmr`j+?jXIojGUb+z;>E4|~=#d(Esp>$lgQ+0QVnR542qZ2juE z5QIfVHCGpM?4}vI;(%OJx=NH#tc* zZ&uIG;E3G&Y$t%VqzkcUQxg-rAyb;~8| zUeUyh33`X#mMy1O^t!}`YtOINbR`{;V5xG|&N@wsUY(*3sAXrlvb3ORQvW)5`tJpp;|@wyfmBOMjC9{=U-xRb}A+ z)|q9KGSmuG$e{?j@I7BJ;u`)A@f!3NJk=s*OVoG`?Nyg{=5n%MG^dye4D|$g-<_wH zXBi(+$HSi@Hp}v?Ln^$xyd!R1^4$T_^<&&n0ac&^Bq*T%j?F$V3@p7hf&*not|+1N z;bO$N`7yTS4{1m`I1$+i2O<2NEt#lPo>GE;KYvOoh@UmXNel+twZj}71N)kJ5^eh7 z0gHv{z+Yl3T*5H_22pfs6bH?^4bMm>96=jU+~DXtlXr9%)^2aTM0d9Us`p?TPr^B_BLs3(GqXs&cE!$Sad4CI4b{C;4jQx2hil1kl7y=R}Ly*R8wFeQ#1v9)J|38p^o%iY>bK z29wA5$tJLEQRA}WLuOH_5#;<#upDeR;a@NT{{rkUa{obOf`uEEjVm)(1E*J&&_{xX zG**2F1hWmHJlH@DM=`;VSP*w+!tc?>Y#7QovSea<@pAAlKaj0vok=B)tKo|4;fN(T zDBz$o&Y#Yru&?(IaYzG!r8B-x7H1Ep<0xH`d=4KH8Tr+wvVe>DBtw_eF~w+z6~g3! zqK~)q_RVm&C0Qc{-o|vr?U&CD4_N#6zjcIVK_0(Smg{tK*P~{Nvc5~yi!M!#zTsQp z;scxMX9;AXf@9eqKwiYrz#2(m-D+R3=IZ8Fy$1a|anH7GBLQt(M=}RXOL|ykUW-BL zw-uFzoF)`+S>p|&C!)lxB|$8E{76v21f6K79Ub;r-l~BUDzI4Ggwp3y`rjn~2c%6- z7}zC?bKrwZ1|J4+;v)p}Ziz=c57DCDSs*EF8Qcw>y1~!d^J9YiGQ^lo3icd{H7CPot zeOz9Mb39DQ&oClm41?)&vc(%g8bAT6v!PGrhzxvY@;IlQ{E4i`OUS*m*vA#k8~37( zi{naJIx5MOJxQV}R7YQ36*!gs;o(me)VDm&=H}tAp_);fM~Ie0Q!!w05D$Qk!Mv)g zt7GSPaPX7DKbF3yO)#Q~0>s>{>m$c!W;lfL8#BAn&@hug<=H2i=^wYZZ9WglD@=z& zLhI|&`S@f!y{?JCPb)aRLg_uV=X94`K3P9ISResWWJ?tig6M}X0H&VoYmY7$_HSMK zE_Y&Ua(@%yCn^=mD;f-+;%LX^Vl&zIgbE9@3j1m~Xt$;YpcOU3 zIq*m&u9WF$O{g$;aCvn`ydFI`Xu;ecfyuwJA&*Cm0j#yOpA;Xw5r_wL?>+Qe&BdCs zGW2kTxw^aY`d|+bs~z@7I4>{n>eZ{uo0G^ZE-r_qBvURk$>)dxASKo(ysRwxs?nMk z2TQDdb2`k^CV7v#?={4lnYl<;#%9@4w;lb_YJQ$`9e8|a?@)>72h9c4E5>a zi^X@Td*wVN(%-JO4cJH*sm2Fz@GkqD?{bGiPNu#39fJSa>Lt=dO}zU+yhvcyr1g8B zhlKw6Lhkq;;Gx99ThfPT^RG2X1K6rXW_5Dq$2$75+B{a2ge`1k@NXDYsM4{Lb~1(o zZ3Nzv^iFYJA42!4tEoBSLQlPSZzLr08Q-`8Gcz-@DLfOGE~#~oT($$p#coOPK~V75 zLct3$mrDx`iYI&0U)R_4vsYI(GWBL<4Ky?oCc}VIiRL~8k@_^AqiHEc&vIo8F*I*s zZTllno(4N+AmT0^C>(F$#zD1}$$5Xi%1HK9bM(*#qQ1ds$2M?l;w(C5%EO^jk?4dr zOA}S~cVxylwX(&ZqrP2`4yv$hLwkwo#PF2qp;LtJgxvL|BFN6l@VzYAwjf$NX>dNX z0*GbaM7!x57A(Do%)uS`By|+yOAXZInE|1#G8~S}U*`}dH&H8sEO8*d{Z;uxExyo} zY*AQdoD6G|=U`Fy0||ATKV&ZYEMT;~y$#AmPp0?3MI*syIf&VlNczs>i(aBrp+euO za@)f0-i#$O`kv}Bwe6ldLh&=ifo3LjlF3E?!z4lfnr=ZK0 zKExjJ>Sp+d%wHgqL6YGy9i2z5RGb2n^7wOpy4EbqNlyFAj@~Z4BGy(GM9j+EdsGY3 zztn}*UTs4+inN%ZY-z(8#CSMNLdIt|2w7D}MM&Q^`8cwC>k`9{+g{uI3-khzDJ{jY zZFdX~4vuVk_Im!uAQtoqEUp^a7$)Ur>FVhjsV2Xwfpnx9F2;d~YPAJ~PrE&MRJzY8T(v1SX|$-Y+76 z-?+~&!*@iD&B2Cxq;Gkh{lN96}zQE!Ga5aJ{|~gAqj# za&0lAh(0XH7ZS^P*3iwN2T7+kAG|_4O-(hRo4W{bc4oo%w~%CD0if93?dKcW_~A9;Pv_2e7#MG3R_-T^kdAeGAVF<4H8Bz3ZvN&?<;0%kKG) zz~qAsHtsG~6+C;sLQ!=tK=x)x)dowhT|meq8k7%hjf*YG)puBZd{Lt3qm3$p^Ke^O zuKxjeS9xk@v6(t5@A_nTW!SY`KhOT?`JA!|;N2X?j>^2jEuB9h_A93#Buw9$;&8i_ zS8ZXKFurCfg+E7ZNSUTa#I=(7j(!^7BX1z|eDs~RS11Y5ZPgj2K!|Fw72zJ7ON>8y z>G(jf| ziK4K3P8Ie(B!_TieDp9*?TA2^`UHAJbYBXm>U|iSE?wE_;p;Lw-4*u!_Y!wMConax zs|B&yf0?sM(yo_R(N22!hAMTlrmi{tn9TK7l6E-dCOK8h=YTxgc`EA^Ep&BLcxtAT z9+wb2*P|Nt=PEZhx42%oqJ+fW^pFU5qIUR_xQK|zRR@Qr+x?;$(gxhrxP*(4m&;3n z`qF&80-Az^c%~=WM47}hCF-dGxG?19L2JP!vb#qS)qZx6i=?Q$h~{ZN;+}(#fg_>d zu1F+1J(*Ttm_WwsRQI0#;{*>r_lNwdk`87KM0&#lEykAlpNzF1fybdj57D=kuWJsx zw|CdAcQG>t@0Ap!jc_k5bcV~&)}>zE4)JL19ag8*S5=Iav~Z`sdo R%z;@3S(@3I)|&8)0VZWOGQ?)LPB|66ut->su+bl@iM**Mbr1iGk36O!#i-CBS(##86j1- z(l#bOn(Ifuc{y!YveGs|e`Kd>%cfTQDvy+g#vTu`P`aM>ctdN}5~0KB$D;>Lf_GVe zUGXaZPFWAztH_oKrpfNICx<6Yx}z6rHzH5@LzaTWec@GtU*WR&j4 z&7bea%2yp*jebsbz9+c3NuH5>YD|?Gc1ZPF-ll$J!a|kSgkG|Vjixdm!IaLBsyHuY z_Q))7c{1=jQL^=9rHgy+rJ2>tk|0cw=g>x(>&91f+%n}4&Ry}_RH)=f%P$kmuSJ2l znh;!V;mFo=4<-4@VX&DEBo<$^ayJ6ha`Npl zAQ0(j^wC%dL`Cm20F1}>4-gO{^A*Nv@8Nj5a0Ev^hTrnj>`d+r!CP zUQ4fUjxfCHp~g>~Eynl`7ZnfX#}+U4B-=lD)e|>5lu%qW8TE*sFbdk41Z;^`!c=>O z>l>1|6-GH0LTc|NmIH4d55dR;|J?xpMh8e_3;9_-K$2Ussxxc87HKCt417|uQu9sk zm%9z`Gawh1XMoqsW64plREkYXc#hh(rltU|1FC(JDSau2GlrpWBc8zLJB~hqqup|R zA((>wa@v@{&xnyN1)bJ;kDXMtcRNP{^W$%!%K={{W}nF6+m_m(=LiM1inA<%HG@k4 zTqv_|9k;vL1mA9h(=0A;cDQoy4}j~3S0Zx~{|*@obdzs)G`xz`h=(p|eaI8|Fj#bG z4@}-kHHQ8eHVS_!Pj@d+Wu!Hb5AtT--RJBPJT6nlyx#cQQi1SI9bhGKOL7_zT|AQ8 zeo&CYX<#_`xV_N55`((N*Xl!2x;QBERzPweGwh-l#XiR|NzY8Z83oqs+@zNc%R83f zPNL2qdEzP@9r~zw0JG(R3Hc3lg_2hx8&>Ma7{^yUxq!yMR&jWDIduHHia+B2@+}T2 z6`sc!+@DRzBFKHl?tpiXY5TdCZV?R64AoqeT+)cv{3$b3qjSq$S{P8J{7fPCq+)Hq#BVm;557Eh z=@3UfsQbZ7cizmD3+HS{1A64=7zH$MUXakSy~~`u{lb@cu+SPn%XeAspI&kqnAoT; zSqzd!URS11hoA3%x=YYHw(RY^IMA57HWP()Uc`f9uTb#pv)rs-4p0gyi`Wls`wpWH zvf(`if>9I%)z_}A21JI8>FrPyi?3L0smDz{d|#_}dco@|TVJA2nN!2~>V<+W{G`qF z5A9cz3nO+M@ zl1ko{)EOXMtxZIFUC-1p26pc)-wJo`Z(nBaYXmGdElE#0C9R+*>_FrCa&S`vc@bK& z;ic5D0_kfjk6%Sd@>Xab4Z0A=i%F*sw$?!Q?2{QzB{L~&*IH}DT~n3qEg`p6cyh>Iixj>yNHwoXOFmt=}}Yw-Widn)N=tz-YVF z#jiPWb2ikl81S*O?;j@J-paJ%p54kx8NxyvDwuGH9ns@cH2z-K z&QgSG77v&JxNmW z*i&MP{P>H}J9W)&hkBr<2o`@-Ao9913?s*Fqi-$_&8yVtTwrjQo%_jaFc{TVzAuxM$_Yt`cVB~bk9IP@ekyA zZw>fh-sUJUVY0E^rwH1kTbuP})h+SDQTbyKTd0Oy4I!q2L|3H)biy%ep!5RaK?|lj5Z$0@0x9hdn&jOlDuYdq30^U>Vpo zw{KwSwLQ$8wW^cwmbnGxQYMyh$FWBssBA~kHi4P|{eb@h*XqB_bZG>L=ZSUiO*l4a z$;t_sFJ&L)^xgvOtTQiCXe@9SbQ8Uv9TcEA`HWIOz(YZi@Eaw24W;C;H zf!_=Ie=qKuJOG~Faw5lrWZQB@o~7M3qXieiumC^3`zfbed2~%9VD?m=9_J*l<4pr6 z#cuVJCnthoFlR)6K57|&%d%e!ahFs%6$IYrL<*Rkvv#Yb73Prvv1iyVX+Xk|G>Duv z2u|IxHdy6rqF*c@jS5?A#9>*Ag8hPrp!8kUC!_j;aE8Gt`{%gb@7~oSE=!j-BGC(` zut!FXp4g17lEo6mI~si{d5D%;#mt3<`ev#p5f_n!Zi{&Bot0$NvU6ps-FjuJi3`tW zbeLZox7EfxJr8U*r2aL)|A+K9a%}#4ls&GcaH_MaPS}euv4S(fP1pSUkcaD2Bn0E^ K?! FilePickerFiles { get; } = - new ObservableCollection(); +private static readonly IList Places = new List +{ + "Austria", + "Belgium", + "Bulgaria", + "Croatia", + "Cyprus", + "Czechia", + "Denmark", + "Estonia", + "Finland", + "France", + "Germany", + "Greece", + "Hungary", + "Ireland", + "Italy", + "Latvia", + "Lithuania", + "Luxembourg", + "Malta", + "Netherlands", + "Poland", + "Portugal", + "Romania", + "Slovakia", + "Slovenia", + "Spain", + "Sweden" +}; -public ICollection FilePickerTypes => new List { ".jpg" }; \ No newline at end of file +public ICommand SuggestionTextChangeCommand => new RelayCommand(this.OnSuggestionTextChanged); + +public ObservableCollection SelectedChips { get; } = new() +{ + new ChipItem("United Kingdom") +}; + +public ObservableCollection ChipSuggestions { get; } = new(Places); + +private void OnSuggestionTextChanged(string obj) +{ + ChipSuggestions.MakeEqualTo(Places.Where(x => x.Contains(obj, StringComparison.CurrentCultureIgnoreCase))); +} diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt index f476519..ad55d03 100644 --- a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Assets/ChipBox/ChipBoxXaml.txt @@ -12,6 +12,9 @@ + Header="ChipBox" + Suggestions="{x:Bind ViewModel.ChipSuggestions}" + Chips="{x:Bind ViewModel.SelectedChips}" + TextChangeCommand="{x:Bind ViewModel.SuggestionTextChangeCommand}"/> diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml index 20a8947..e12dd78 100644 --- a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/Pages/ChipBoxPage.xaml @@ -45,7 +45,7 @@ Grid.Row="1" Margin="0,0,0,48" Style="{StaticResource BaseTextBlockStyle}" - Text="The FilePicker is a custom-built UI element that provides a file selection user experience. The control works in a similar way to the file input element in web applications." /> + Text="The ChipBox is a custom-built UI element that provides multi value input for a text box with auto-suggest capabilities. Values added are displayed as removable chips." /> @@ -54,11 +54,21 @@ SampleName="A multi-value input control using chips" XamlSource="ChipBox/ChipBoxXaml.txt"> - + + + + + + + Chips="{x:Bind ViewModel.SelectedChips}" + TextChangeCommand="{x:Bind ViewModel.SuggestionTextChangeCommand}"/> + + + diff --git a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs index 7042a42..e3768f0 100644 --- a/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs +++ b/samples/MADE.Samples/MADE.Samples.Shared/Features/Samples/ViewModels/ChipBoxPageViewModel.cs @@ -1,22 +1,20 @@ namespace MADE.Samples.Features.Samples.ViewModels { + using System; using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Linq; + using System.Windows.Input; + using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; + using MADE.Collections; using MADE.UI.Controls; using MADE.UI.Views.Navigation; using MADE.UI.Views.Navigation.ViewModels; public class ChipBoxPageViewModel : PageViewModel { - public ChipBoxPageViewModel(INavigationService navigationService, IMessenger messenger) - : base(navigationService, messenger) - { - } - - public ObservableCollection SelectedChips { get; } = new ObservableCollection(); - - public ICollection ChipSuggestions => new List + private static readonly IList Places = new List { "Austria", "Belgium", @@ -46,5 +44,31 @@ public ChipBoxPageViewModel(INavigationService navigationService, IMessenger mes "Spain", "Sweden" }; + + public ChipBoxPageViewModel(INavigationService navigationService, IMessenger messenger) + : base(navigationService, messenger) + { + } + + public ICommand AddChipCommand => new RelayCommand(this.AddChip); + + public ICommand SuggestionTextChangeCommand => new RelayCommand(this.OnSuggestionTextChanged); + + public ObservableCollection SelectedChips { get; } = new() + { + new ChipItem("United Kingdom") + }; + + public ObservableCollection ChipSuggestions { get; } = new(Places); + + private void OnSuggestionTextChanged(string obj) + { + ChipSuggestions.MakeEqualTo(Places.Where(x => x.Contains(obj, StringComparison.CurrentCultureIgnoreCase))); + } + + private void AddChip() + { + this.SelectedChips.Add(new ChipItem("Global")); + } } } \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/Chip.cs b/src/MADE.UI.Controls.ChipBox/Chip.cs index 6ea6894..c286fe5 100644 --- a/src/MADE.UI.Controls.ChipBox/Chip.cs +++ b/src/MADE.UI.Controls.ChipBox/Chip.cs @@ -1,3 +1,6 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + namespace MADE.UI.Controls { using System.Windows.Input; @@ -5,16 +8,25 @@ namespace MADE.UI.Controls using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; + /// + /// Defines a control for displaying a value as a chip with remove capabilities. + /// [TemplatePart(Name = ChipContentPart, Type = typeof(ContentPresenter))] [TemplatePart(Name = ChipRemoveButtonPart, Type = typeof(Button))] public sealed partial class Chip : ContentControl, IChip { + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty RemoveCommandProperty = DependencyProperty.Register( nameof(RemoveCommand), typeof(ICommand), typeof(Chip), new PropertyMetadata(default(ICommand))); + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty CanRemoveProperty = DependencyProperty.Register( nameof(CanRemove), typeof(bool), @@ -24,25 +36,40 @@ public sealed partial class Chip : ContentControl, IChip private const string ChipContentPart = "ChipContent"; private const string ChipRemoveButtonPart = "ChipRemoveButton"; + /// + /// Initializes a new instance of the class. + /// public Chip() { this.DefaultStyleKey = typeof(Chip); } - public event ChipRemoveEventHandler Removed; + /// + /// Occurs when the user pressed the remove chip button. + /// + public event ChipRemovedEventHandler Removed; + /// + /// Gets or sets the associated with when the user pressed the remove chip button. + /// public ICommand RemoveCommand { get => (ICommand)GetValue(RemoveCommandProperty); set => SetValue(RemoveCommandProperty, value); } + /// + /// Gets or sets a value indicating whether the chip can be removed. + /// public bool CanRemove { get => (bool)GetValue(CanRemoveProperty); set => SetValue(CanRemoveProperty, value); } + /// + /// Gets the view representing the remove chip button. + /// public Button RemoveButton { get; private set; } /// @@ -70,7 +97,7 @@ protected override void OnApplyTemplate() private void OnRemoveClick(object sender, RoutedEventArgs e) { this.RemoveCommand?.Execute(this.Content); - this.Removed?.Invoke(this, new ChipRemoveEventArgs(this.Content)); + this.Removed?.Invoke(this, new ChipRemovedEventArgs(this.Content)); } private void SetRemoveButtonVisibility() diff --git a/src/MADE.UI.Controls.ChipBox/ChipBox.cs b/src/MADE.UI.Controls.ChipBox/ChipBox.cs index fb0527c..eeede25 100644 --- a/src/MADE.UI.Controls.ChipBox/ChipBox.cs +++ b/src/MADE.UI.Controls.ChipBox/ChipBox.cs @@ -5,7 +5,10 @@ namespace MADE.UI.Controls { using System.Collections.Generic; using System.Collections.ObjectModel; + using System.Collections.Specialized; using System.Linq; + using System.Windows.Input; + using MADE.UI.Extensions; using Windows.UI.Xaml; using Windows.UI.Xaml.Automation.Peers; using Windows.UI.Xaml.Controls; @@ -26,6 +29,15 @@ public partial class ChipBox : Control, IChipBox typeof(ChipBox), new PropertyMetadata(default)); + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty HeaderTemplateProperty = DependencyProperty.Register( + nameof(HeaderTemplate), + typeof(DataTemplate), + typeof(ChipBox), + new PropertyMetadata(null)); + /// /// Identifies the dependency property. /// @@ -33,7 +45,16 @@ public partial class ChipBox : Control, IChipBox nameof(Chips), typeof(IList), typeof(ChipBox), - new PropertyMetadata(new ObservableCollection())); + new PropertyMetadata(new ObservableCollection(), (o, args) => ((ChipBox)o).SetupChips(args.OldValue as IList, args.NewValue as IList))); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ChipContentTemplateProperty = DependencyProperty.Register( + nameof(ChipContentTemplate), + typeof(DataTemplate), + typeof(ChipBox), + new PropertyMetadata(default(DataTemplate), (o, args) => ((ChipBox)o).UpdateChipContentTemplates())); /// /// Identifies the dependency property. @@ -44,23 +65,68 @@ public partial class ChipBox : Control, IChipBox typeof(ChipBox), new PropertyMetadata(default(Style))); + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty SuggestionsProperty = DependencyProperty.Register( nameof(Suggestions), typeof(object), typeof(ChipBox), new PropertyMetadata(default)); + /// + /// Identifies the dependency property. + /// public static readonly DependencyProperty SuggestionsItemTemplateProperty = DependencyProperty.Register( nameof(SuggestionsItemTemplate), typeof(DataTemplate), typeof(ChipBox), new PropertyMetadata(default(DataTemplate))); - public static readonly DependencyProperty AllowDuplicatesProperty = DependencyProperty.Register( - nameof(AllowDuplicates), + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AllowDuplicateProperty = DependencyProperty.Register( + nameof(AllowDuplicate), + typeof(bool), + typeof(ChipBox), + new PropertyMetadata(true)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty AllowFreeTextProperty = DependencyProperty.Register( + nameof(AllowFreeText), + typeof(bool), + typeof(ChipBox), + new PropertyMetadata(true)); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty IsReadonlyProperty = DependencyProperty.Register( + nameof(IsReadonly), typeof(bool), typeof(ChipBox), - new PropertyMetadata(default(bool))); + new PropertyMetadata(default(bool), (o, args) => ((ChipBox)o).UpdateForReadonly())); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty TextChangeCommandProperty = DependencyProperty.Register( + nameof(TextChangeCommand), + typeof(ICommand), + typeof(ChipBox), + new PropertyMetadata(default(ICommand))); + + /// + /// Identifies the dependency property. + /// + public static readonly DependencyProperty ChipRemoveCommandProperty = DependencyProperty.Register( + nameof(ChipRemoveCommand), + typeof(ICommand), + typeof(ChipBox), + new PropertyMetadata(default(ICommand))); private const string ChipBoxTextBoxPart = "ChipBoxTextBox"; @@ -74,6 +140,16 @@ public ChipBox() this.DefaultStyleKey = typeof(ChipBox); } + /// + /// Occurs when the text has changed within the suggestion box. + /// + public event ChipBoxTextChangedEventHandler TextChanged; + + /// + /// Occurs when a chip has been removed. + /// + public event ChipBoxChipRemovedEventHandler ChipRemoved; + /// /// Gets or sets the content for the control's header. /// @@ -83,6 +159,15 @@ public object Header set => this.SetValue(HeaderProperty, value); } + /// + /// Gets or sets the template used to display the content of the control's header. + /// + public DataTemplate HeaderTemplate + { + get => (DataTemplate)this.GetValue(HeaderTemplateProperty); + set => this.SetValue(HeaderTemplateProperty, value); + } + /// /// Gets or sets an object source used to generate the chip content of the control. /// @@ -92,6 +177,15 @@ public IList Chips set => this.SetValue(ChipsProperty, value); } + /// + /// Gets or sets the template associated with the content displayed in the chip. + /// + public DataTemplate ChipContentTemplate + { + get => (DataTemplate)GetValue(ChipContentTemplateProperty); + set => SetValue(ChipContentTemplateProperty, value); + } + /// /// Gets or sets the style of the items view. /// @@ -101,22 +195,76 @@ public Style ChipItemsViewStyle set => this.SetValue(ChipItemsViewStyleProperty, value); } + /// + /// Gets or sets the suggestions that are displayed to the user when typing. + /// public object Suggestions { get => GetValue(SuggestionsProperty); set => SetValue(SuggestionsProperty, value); } + /// + /// Gets or sets the template used to display the suggestions that are displayed to the user when typing. + /// public DataTemplate SuggestionsItemTemplate { get => (DataTemplate)GetValue(SuggestionsItemTemplateProperty); set => SetValue(SuggestionsItemTemplateProperty, value); } - public bool AllowDuplicates + /// + /// Gets or sets a value indicating whether to allow duplicate values to be accepted. + /// + /// + /// The default value is True. + /// + public bool AllowDuplicate + { + get => (bool)GetValue(AllowDuplicateProperty); + set => SetValue(AllowDuplicateProperty, value); + } + + /// + /// Gets or sets a value indicating whether to allow free text input. + /// + /// + /// The default value is True. + /// + public bool AllowFreeText + { + get => (bool)GetValue(AllowFreeTextProperty); + set => SetValue(AllowFreeTextProperty, value); + } + + /// + /// Gets or sets a value indicating whether the control is in a read-only state. + /// + /// + /// The default value is False. + /// + public bool IsReadonly + { + get => (bool)GetValue(IsReadonlyProperty); + set => SetValue(IsReadonlyProperty, value); + } + + /// + /// Gets or sets the associated with when the text has changed within the suggestion box. + /// + public ICommand TextChangeCommand + { + get => (ICommand)GetValue(TextChangeCommandProperty); + set => SetValue(TextChangeCommandProperty, value); + } + + /// + /// Gets or sets the associated with when a chip has been removed. + /// + public ICommand ChipRemoveCommand { - get => (bool)GetValue(AllowDuplicatesProperty); - set => SetValue(AllowDuplicatesProperty, value); + get => (ICommand)GetValue(ChipRemoveCommandProperty); + set => SetValue(ChipRemoveCommandProperty, value); } /// @@ -124,6 +272,9 @@ public bool AllowDuplicates /// public AutoSuggestBox TextBox { get; private set; } + /// + /// Gets the view representing the chip items. + /// public ItemsControl ChipItemsView { get; private set; } /// @@ -133,6 +284,7 @@ protected override void OnApplyTemplate() { if (this.TextBox != null) { + this.TextBox.TextChanged -= OnChipSuggestionTextChanged; this.TextBox.QuerySubmitted -= OnChipSuggestionChosen; } @@ -143,8 +295,12 @@ protected override void OnApplyTemplate() if (this.TextBox != null) { + this.TextBox.TextChanged += this.OnChipSuggestionTextChanged; this.TextBox.QuerySubmitted += this.OnChipSuggestionChosen; } + + this.SetupChips(this.Chips, this.Chips); + this.UpdateForReadonly(); } /// @@ -158,11 +314,79 @@ protected override AutomationPeer OnCreateAutomationPeer() private void OnChipSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) { + if (!this.AllowFreeText && args.ChosenSuggestion == null) + { + return; + } + this.AddChip(args.QueryText); - if (sender != null) + if (sender == null) { - sender.Text = string.Empty; + return; } + + sender.Text = string.Empty; + sender.IsSuggestionListOpen = false; + } + + private void OnChipSuggestionTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if (sender == null) + { + return; + } + + this.TextChanged?.Invoke(this, new ChipBoxTextChangedEventArgs(sender.Text)); + this.TextChangeCommand?.Execute(sender.Text); + } + + private void SetupChips(IEnumerable previousChips, IList newChips) + { + this.UnhookObservableChipsEvent(previousChips); + this.HookObservableChipsEvent(newChips); + + if (this.ChipItemsView is not { Items: { } }) + { + return; + } + + this.ChipItemsView.Items.Clear(); + + foreach (var chipItem in newChips) + { + this.AddChipItemToView(chipItem); + } + } + + private void OnChipItemsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + { + foreach (var item in e.NewItems) + { + this.AddChipItemToView(item as ChipItem); + } + + break; + } + + case NotifyCollectionChangedAction.Remove: + { + foreach (var item in e.OldItems) + { + this.RemoveChipItemFromView(item as ChipItem); + } + + break; + } + } + } + + private void OnChipRemoved(object sender, ChipRemovedEventArgs args) + { + this.RemoveChip(args.Content, sender as Chip); } private void AddChip(object content) @@ -172,26 +396,80 @@ private void AddChip(object content) return; } - if (this.ValidateDuplicates(content)) + if (this.ValidateDuplicate(content)) { return; } - var chipItem = new ChipItem { Content = content }; + var chipItem = new ChipItem(content); - if (this.ChipItemsView is { Items: { } }) + this.AddChipItemToView(chipItem); + + this.UnhookObservableChipsEvent(this.Chips); + this.Chips?.Add(chipItem); + this.HookObservableChipsEvent(this.Chips); + } + + private void RemoveChip(object content, Chip originator = null) + { + if (content == null && originator == null) { - var chip = new Chip { Content = chipItem.Content }; - this.ChipItemsView.Items.Add(chip); - chip.Removed += OnChipRemoved; + return; } - this.Chips?.Add(chipItem); + var chipItem = this.Chips?.FirstOrDefault(x => x.Content.Equals(content)); + if (chipItem != null) + { + this.UnhookObservableChipsEvent(this.Chips); + this.Chips.Remove(chipItem); + this.HookObservableChipsEvent(this.Chips); + } + + if (originator is { } chip) + { + chip.Removed -= this.OnChipRemoved; + this.ChipItemsView?.Items?.Remove(chip); + } + + this.ChipRemoved?.Invoke(this, new ChipBoxChipRemovedEventArgs(content)); + this.ChipRemoveCommand?.Execute(content); } - private bool ValidateDuplicates(object content) + private void AddChipItemToView(ChipItem chipItem) { - if (this.AllowDuplicates) + if (chipItem == null || this.ChipItemsView is not { Items: { } }) + { + return; + } + + if (chipItem.Content == null || string.IsNullOrWhiteSpace(chipItem.Content.ToString())) + { + return; + } + + if (this.ValidateDuplicate(chipItem.Content)) + { + return; + } + + var chip = new Chip { Content = chipItem.Content, ContentTemplate = this.ChipContentTemplate }; + this.ChipItemsView.Items.Add(chip); + chip.Removed += this.OnChipRemoved; + } + + private void RemoveChipItemFromView(ChipItem item) + { + if (item?.Content == null) + { + return; + } + + RemoveChip(item.Content); + } + + private bool ValidateDuplicate(object content) + { + if (this.AllowDuplicate) { return false; } @@ -210,18 +488,47 @@ private bool ValidateDuplicates(object content) return existingChipItem != null; } - private void OnChipRemoved(object sender, ChipRemoveEventArgs args) + private void UpdateChipContentTemplates() { - var chipItem = this.Chips?.FirstOrDefault(x => x.Content.Equals(args.Item)); - if (chipItem != null) + if (this.ChipItemsView is not { Items: { } }) { - this.Chips.Remove(chipItem); + return; } - if (sender is Chip chip) + foreach (var chip in this.ChipItemsView.Items.Cast()) { - chip.Removed -= this.OnChipRemoved; - this.ChipItemsView?.Items?.Remove(chip); + chip.ContentTemplate = this.ChipContentTemplate; + } + } + + private void UpdateForReadonly() + { + this.TextBox.SetVisible(!this.IsReadonly); + + if (this.ChipItemsView is not { Items: { } }) + { + return; + } + + foreach (var chip in this.ChipItemsView.Items.Cast()) + { + chip.CanRemove = !this.IsReadonly; + } + } + + private void HookObservableChipsEvent(IEnumerable chips) + { + if (chips is ObservableCollection observableChips) + { + observableChips.CollectionChanged += this.OnChipItemsChanged; + } + } + + private void UnhookObservableChipsEvent(IEnumerable chips) + { + if (chips is ObservableCollection observableChips) + { + observableChips.CollectionChanged -= this.OnChipItemsChanged; } } } diff --git a/src/MADE.UI.Controls.ChipBox/ChipBoxAutomationPeer.cs b/src/MADE.UI.Controls.ChipBox/ChipBoxAutomationPeer.cs index c3546c6..3e44ab0 100644 --- a/src/MADE.UI.Controls.ChipBox/ChipBoxAutomationPeer.cs +++ b/src/MADE.UI.Controls.ChipBox/ChipBoxAutomationPeer.cs @@ -29,7 +29,7 @@ public ChipBoxAutomationPeer(ChipBox owner) /// The control type. protected override AutomationControlType GetAutomationControlTypeCore() { - return AutomationControlType.Text; + return AutomationControlType.List; } /// @@ -66,21 +66,5 @@ protected override string GetNameCore() return name; } - - /// - /// Provides the interaction patterns associated with the when a Microsoft UI Automation client calls GetPattern or an equivalent Microsoft UI Automation client API. - /// - /// A value from the PatternInterface enumeration. - /// This if it supports the pattern interface; otherwise, null. - protected override object GetPatternCore(PatternInterface patternInterface) - { - switch (patternInterface) - { - case PatternInterface.Text: - return this; - } - - return base.GetPatternCore(patternInterface); - } } } \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventArgs.cs b/src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventArgs.cs new file mode 100644 index 0000000..603d088 --- /dev/null +++ b/src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventArgs.cs @@ -0,0 +1,26 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.UI.Controls +{ + using Windows.UI.Xaml; + + /// + /// Defines an event argument for when a is removed from a . + /// + public class ChipBoxChipRemovedEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class. + /// + public ChipBoxChipRemovedEventArgs(object content) + { + this.Content = content; + } + + /// + /// Gets the content of the chip that has been removed. + /// + public object Content { get; } + } +} \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventHandler.cs b/src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventHandler.cs new file mode 100644 index 0000000..07bf95a --- /dev/null +++ b/src/MADE.UI.Controls.ChipBox/ChipBoxChipRemovedEventHandler.cs @@ -0,0 +1,12 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.UI.Controls +{ + /// + /// Defines a delegate for the event which occurs when is removed from a . + /// + /// The . + /// The event argument. + public delegate void ChipBoxChipRemovedEventHandler(object sender, ChipBoxChipRemovedEventArgs args); +} diff --git a/src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventArgs.cs b/src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventArgs.cs new file mode 100644 index 0000000..d250af9 --- /dev/null +++ b/src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventArgs.cs @@ -0,0 +1,27 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.UI.Controls +{ + using Windows.UI.Xaml; + + /// + /// Defines an event argument for when the text of the has changed. + /// + public class ChipBoxTextChangedEventArgs : RoutedEventArgs + { + /// + /// Initializes a new instance of the class with the changed text. + /// + /// The text that has been changed to. + public ChipBoxTextChangedEventArgs(string text) + { + this.Text = text; + } + + /// + /// Gets the text that has been changed to. + /// + public string Text { get; } + } +} \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventHandler.cs b/src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventHandler.cs new file mode 100644 index 0000000..8ced488 --- /dev/null +++ b/src/MADE.UI.Controls.ChipBox/ChipBoxTextChangedEventHandler.cs @@ -0,0 +1,12 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.UI.Controls +{ + /// + /// Defines a delegate for the event which occurs when the text of the has changed. + /// + /// The . + /// The event argument. + public delegate void ChipBoxTextChangedEventHandler(object sender, ChipBoxTextChangedEventArgs args); +} diff --git a/src/MADE.UI.Controls.ChipBox/ChipItem.cs b/src/MADE.UI.Controls.ChipBox/ChipItem.cs index c3c41de..15756fd 100644 --- a/src/MADE.UI.Controls.ChipBox/ChipItem.cs +++ b/src/MADE.UI.Controls.ChipBox/ChipItem.cs @@ -1,7 +1,25 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + namespace MADE.UI.Controls { + /// + /// Defines a wrapper for an item displayed as a chip. + /// public class ChipItem { + /// + /// Initializes a new instance of the class with the associated content. + /// + /// The content of the chip to display. + public ChipItem(object content) + { + this.Content = content; + } + + /// + /// Gets or sets the content of the chip to display. + /// public object Content { get; set; } /// Returns a string that represents the current object. diff --git a/src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs b/src/MADE.UI.Controls.ChipBox/ChipRemovedEventArgs.cs similarity index 58% rename from src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs rename to src/MADE.UI.Controls.ChipBox/ChipRemovedEventArgs.cs index 37f5924..529fd79 100644 --- a/src/MADE.UI.Controls.ChipBox/ChipRemoveEventArgs.cs +++ b/src/MADE.UI.Controls.ChipBox/ChipRemovedEventArgs.cs @@ -8,16 +8,19 @@ namespace MADE.UI.Controls /// /// Defines an event argument for when a is removed. /// - public class ChipRemoveEventArgs : RoutedEventArgs + public class ChipRemovedEventArgs : RoutedEventArgs { /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - public ChipRemoveEventArgs(object item) + public ChipRemovedEventArgs(object content) { - this.Item = item; + this.Content = content; } - public object Item { get; } + /// + /// Gets the content of the chip that has been removed. + /// + public object Content { get; } } } \ No newline at end of file diff --git a/src/MADE.UI.Controls.ChipBox/ChipRemoveEventHandler.cs b/src/MADE.UI.Controls.ChipBox/ChipRemovedEventHandler.cs similarity index 81% rename from src/MADE.UI.Controls.ChipBox/ChipRemoveEventHandler.cs rename to src/MADE.UI.Controls.ChipBox/ChipRemovedEventHandler.cs index c39e392..6222542 100644 --- a/src/MADE.UI.Controls.ChipBox/ChipRemoveEventHandler.cs +++ b/src/MADE.UI.Controls.ChipBox/ChipRemovedEventHandler.cs @@ -8,5 +8,5 @@ namespace MADE.UI.Controls /// /// The . /// The event argument. - public delegate void ChipRemoveEventHandler(object sender, ChipRemoveEventArgs args); + public delegate void ChipRemovedEventHandler(object sender, ChipRemovedEventArgs args); } diff --git a/src/MADE.UI.Controls.ChipBox/IChip.cs b/src/MADE.UI.Controls.ChipBox/IChip.cs index 525508a..b8b2fca 100644 --- a/src/MADE.UI.Controls.ChipBox/IChip.cs +++ b/src/MADE.UI.Controls.ChipBox/IChip.cs @@ -1,13 +1,28 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + namespace MADE.UI.Controls { using System.Windows.Input; + /// + /// Defines an interface for displaying a value as a chip with remove capabilities. + /// public interface IChip { - event ChipRemoveEventHandler Removed; + /// + /// Occurs when the user pressed the remove chip button. + /// + event ChipRemovedEventHandler Removed; + /// + /// Gets or sets the associated with when the user pressed the remove chip button. + /// ICommand RemoveCommand { get; set; } + /// + /// Gets or sets a value indicating whether the chip can be removed. + /// bool CanRemove { get; set; } } } diff --git a/src/MADE.UI.Controls.ChipBox/IChipBox.cs b/src/MADE.UI.Controls.ChipBox/IChipBox.cs index 5aa8e18..49db0ca 100644 --- a/src/MADE.UI.Controls.ChipBox/IChipBox.cs +++ b/src/MADE.UI.Controls.ChipBox/IChipBox.cs @@ -3,10 +3,9 @@ namespace MADE.UI.Controls { - using System.Collections; using System.Collections.Generic; + using System.Windows.Input; using Windows.UI.Xaml; - using Windows.UI.Xaml.Controls; /// /// Defines an interface for a multi value input text box control. @@ -14,22 +13,73 @@ namespace MADE.UI.Controls public interface IChipBox { /// - /// Gets or sets the content for the control's header. + /// Occurs when the text has changed within the suggestion box. + /// + event ChipBoxTextChangedEventHandler TextChanged; + + /// + /// Occurs when a chip has been removed. + /// + event ChipBoxChipRemovedEventHandler ChipRemoved; + + /// + /// Gets or sets the associated with when the text has changed within the suggestion box. + /// + ICommand TextChangeCommand { get; set; } + + /// + /// Gets or sets the associated with when a chip has been removed. + /// + ICommand ChipRemoveCommand { get; set; } + + /// + /// Gets or sets the data used for the header of each control. /// object Header { get; set; } + /// + /// Gets or sets the template used to display the content of the control's header. + /// + DataTemplate HeaderTemplate { get; set; } + /// /// Gets or sets an object source used to generate the chip content of the control. /// IList Chips { get; set; } + /// + /// Gets or sets the template associated with the content displayed in the chip. + /// + DataTemplate ChipContentTemplate { get; set; } + /// /// Gets or sets the style of the items view. /// Style ChipItemsViewStyle { get; set; } + /// + /// Gets or sets the suggestions that are displayed to the user when typing. + /// object Suggestions { get; set; } + /// + /// Gets or sets the template used to display the suggestions that are displayed to the user when typing. + /// DataTemplate SuggestionsItemTemplate { get; set; } + + /// + /// Gets or sets a value indicating whether to allow duplicate values to be accepted. + /// + bool AllowDuplicate { get; set; } + + /// + /// Gets or sets a value indicating whether to allow free text input. + /// + bool AllowFreeText { get; set; } + + /// + /// Gets or sets a value indicating whether the control is in a read-only state. + /// + bool IsReadonly { get; set; } } } diff --git a/src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj b/src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj index e50730c..f7daba5 100644 --- a/src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj +++ b/src/MADE.UI.Controls.ChipBox/MADE.UI.Controls.ChipBox.csproj @@ -7,8 +7,9 @@ This package includes UI components for Windows and Uno Platform applications such as: - ChipBox for providing a chip input control that allows users to enter multiple values into an input. + - Chip for providing a single chip control instance that can be used to display values as chips. - MADE UI Views Controls UWP Chips ChipBox TextBox + MADE UI Views Controls UWP Chips ChipBox TextBox AutoSuggest true MADE.UI.Controls.ChipBoxControl diff --git a/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml b/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml index 48c0ec7..d55bd2f 100644 --- a/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml +++ b/src/MADE.UI.Controls.ChipBox/Themes/Generic.xaml @@ -4,7 +4,7 @@ xmlns:local="using:MADE.UI.Controls">