├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CI_CD ├── BuildAndPublish.yml └── azure-pipelines.yml ├── Docs ├── 0_App.png ├── 1_Control.png ├── 2_Control.png ├── App2.0_2.gif ├── App2.0_Screenshot_1.png ├── AppScreenShot.png ├── Demo_App_Code_v1.md ├── Logo.png ├── MultiSelectCombobox_Design_Doc_v1.md ├── Version_History.md ├── copy_paste-min.gif ├── drag_drop-min.gif └── drag_drop_2-min.gif ├── LICENSE ├── README.md ├── Source ├── Common │ ├── BlackPearl.Controls.Common.csproj │ ├── Contract │ │ └── ILookUpContract.cs │ └── Extension │ │ ├── GeneralExtension.cs │ │ └── ReflectionExtension.cs ├── CoreLibrary │ ├── AssemblyInfo.cs │ ├── Behavior │ │ └── ListBoxItemBehavior.cs │ ├── BlackPearl.Controls.CoreLibrary.csproj │ ├── BlackPearl.Controls.CoreLibrary.nuspec │ ├── Controls │ │ └── MultiSelectCombobox │ │ │ ├── BasicStructure.cs │ │ │ ├── CustomTypes.cs │ │ │ ├── EntensionMethods.cs │ │ │ └── Implementation.cs │ ├── LookUpContracts │ │ ├── DefaultLookUpContract.cs │ │ └── DiacriticLookUpContract.cs │ ├── MarkupExtension │ │ └── EnumBindingSource.cs │ └── Themes │ │ ├── Defaults.xaml │ │ ├── Generic.xaml │ │ └── MultiSelectComboboxStyle.xaml ├── Demo │ ├── App.xaml │ ├── App.xaml.cs │ ├── BlackPearl.Controls.Demo.csproj │ ├── LookUpContracts │ │ ├── AdvanceLookUpContract.cs │ │ └── SimpleLookUpContract.cs │ ├── Resources │ │ ├── Constants.cs │ │ ├── DefaultStyle.xaml │ │ └── PersonDataProvider.cs │ ├── ShellWindow.xaml │ ├── ShellWindowViewModel.cs │ └── Views │ │ ├── MultiSelectComboBoxDemoView.xaml │ │ └── MultiSelectComboBoxDemoViewModel.cs ├── Directory.Build.props ├── Directory.Packages.props ├── Mahapps │ ├── BlackPearl.Mahapps.csproj │ ├── RegionAdapter │ │ └── HamburgerMenuSingleRegionAdapter.cs │ ├── Resources │ │ └── DefaultStyle.xaml │ ├── UI_Base │ │ ├── BlackPearlDialogWindow.cs │ │ └── BlackPearlMetroWindow.cs │ ├── Utility │ │ ├── BlackPearlThemeManager.cs │ │ └── Extensions.cs │ └── View │ │ ├── ThemeView.xaml │ │ └── ThemeViewModel.cs ├── MainSolution.sln └── Prism │ ├── BlackPearl.Prism.csproj │ ├── Services │ └── DispatcherService.cs │ ├── UI_Base │ ├── BlackPearlUserControl.cs │ ├── BlackPearlViewModel.cs │ └── BlackPearlWindow.cs │ └── Utility │ └── Extensions.cs └── _config.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'BlackPearl | Bug | ' 5 | labels: bug 6 | assignees: nilayjoshi89 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'BlackPearl | Feature | ' 5 | labels: enhancement 6 | assignees: nilayjoshi89 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb -------------------------------------------------------------------------------- /CI_CD/BuildAndPublish.yml: -------------------------------------------------------------------------------- 1 | # .NET Desktop 2 | # Build and run tests for .NET Desktop or Windows classic desktop solutions. 3 | # Add steps that publish symbols, save build artifacts, and more: 4 | # https://docs.microsoft.com/azure/devops/pipelines/apps/windows/dot-net 5 | 6 | trigger: none 7 | 8 | pool: 9 | vmImage: 'windows-latest' 10 | 11 | variables: 12 | solution: '**/*.sln' 13 | buildPlatform: 'Any CPU' 14 | buildConfiguration: 'Release' 15 | 16 | steps: 17 | - task: NuGetToolInstaller@1 18 | 19 | - task: NuGetCommand@2 20 | inputs: 21 | restoreSolution: '$(solution)' 22 | 23 | - task: VSBuild@1 24 | inputs: 25 | solution: '$(solution)' 26 | platform: '$(buildPlatform)' 27 | configuration: '$(buildConfiguration)' 28 | 29 | - task: VSTest@2 30 | inputs: 31 | platform: '$(buildPlatform)' 32 | configuration: '$(buildConfiguration)' 33 | 34 | - task: NuGetCommand@2 35 | inputs: 36 | command: 'pack' 37 | packagesToPack: '**/BlackPearl.Controls.CoreLibrary.nuspec' 38 | versioningScheme: 'off' 39 | 40 | - task: NuGetCommand@2 41 | inputs: 42 | command: 'push' 43 | packagesToPush: '$(Build.ArtifactStagingDirectory)/**/*.nupkg;!$(Build.ArtifactStagingDirectory)/**/*.symbols.nupkg' 44 | nuGetFeedType: 'external' 45 | publishFeedCredentials: 'NuGet_Publish' -------------------------------------------------------------------------------- /CI_CD/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | 4 | pool: 5 | vmImage: 'windows-latest' 6 | 7 | variables: 8 | solution: '**/*.sln' 9 | buildPlatform: 'Any CPU' 10 | buildConfiguration: 'Release' 11 | 12 | steps: 13 | - task: NuGetToolInstaller@1 14 | 15 | - task: NuGetCommand@2 16 | inputs: 17 | restoreSolution: '$(solution)' 18 | 19 | - task: VSBuild@1 20 | inputs: 21 | solution: '$(solution)' 22 | platform: '$(buildPlatform)' 23 | configuration: '$(buildConfiguration)' 24 | 25 | - task: VSTest@2 26 | inputs: 27 | platform: '$(buildPlatform)' 28 | configuration: '$(buildConfiguration)' -------------------------------------------------------------------------------- /Docs/0_App.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/0_App.png -------------------------------------------------------------------------------- /Docs/1_Control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/1_Control.png -------------------------------------------------------------------------------- /Docs/2_Control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/2_Control.png -------------------------------------------------------------------------------- /Docs/App2.0_2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/App2.0_2.gif -------------------------------------------------------------------------------- /Docs/App2.0_Screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/App2.0_Screenshot_1.png -------------------------------------------------------------------------------- /Docs/AppScreenShot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/AppScreenShot.png -------------------------------------------------------------------------------- /Docs/Demo_App_Code_v1.md: -------------------------------------------------------------------------------- 1 | ## Explaining Demo Application Code 2 | 3 | Defining Person: 4 | 5 | ```csharp 6 | public class Person 7 | { 8 | public string Name { get; set; } 9 | public string Company { get; internal set; } 10 | public string City { get; internal set; } 11 | public string Zip { get; internal set; } 12 | public string Info 13 | { 14 | get => $"{Name} - {Company}({Zip})"; 15 | } 16 | } 17 | ``` 18 | 19 | ### 1) Simple Scenario (most common) 20 | We're setting `DisplayMemberPath` to 'Name' value to display `Name` of `Person` in control. We only need to define `ItemSource` and `SelectedItems` bindings. That's it! 21 | 22 | ![](Docs//1_Control.png) 23 | 24 | #### .XAML code: 25 | 26 | ```xml 27 | 31 | ``` 32 | 33 | ### 2) Advance Scenario 34 | If we want filtering on more than one property or need different search/filter strategy. And/or also want to support creation of new Person from MultiSelectCombobox itself. 35 | 36 | ![](Docs/2_Control.png) 37 | 38 | #### .XAML code: 39 | 40 | ```xml 41 | 46 | ``` 47 | 48 | 49 | In Xaml, we have set DisplayMemberPath to Info property. Info is set to return Name, Company and ZipCode. 50 | 51 | **`AdvanceLookUpContract.cs`:** 52 | In this implementation, we have modified search to respect 3 properties on Person. If any of these 3 properties contain search string, item will be shown in Suggestion drop-down. Item is selected from ItemSource based on Name property. We have also set SupportsNewObjectCreation to true which means we can create new Person object using control. CreateObject is written to parse string in format `{Name},{Company},{Zip}`. By inputting string in this format ending with ItemSeparator, it will try to create an object out of inputted string. If it fails to create, it will remove User inputted string from UI. If it succeeds to create object, it will add newly created object to UI and SelectedItems after removing User entered text from UI. 53 | 54 | **[Please note that following implementation is just for demonstration purpose of LookUpContract functionality. This implementation is not efficient and has lot of scope for improvements.]** 55 | 56 | ```csharp 57 | public class AdvanceLookUpContract : ILookUpContract 58 | { 59 | public bool SupportsNewObjectCreation => true; 60 | 61 | public object CreateObject(object sender, string searchString) 62 | { 63 | if (searchString?.Count(c => c == ',') != 2) 64 | { 65 | return null; 66 | } 67 | 68 | int firstIndex = searchString.IndexOf(','); 69 | int lastIndex = searchString.LastIndexOf(','); 70 | 71 | return new Person() 72 | { 73 | Name = searchString.Substring(0, firstIndex), 74 | Company = searchString.Substring(firstIndex + 1, lastIndex - firstIndex - 1), 75 | Zip = searchString.Length >= lastIndex ? searchString.Substring(lastIndex + 1) : string.Empty 76 | }; 77 | } 78 | 79 | public bool IsItemEqualToString(object sender, object item, string seachString) 80 | { 81 | if (!(item is Person std)) 82 | { 83 | return false; 84 | } 85 | 86 | return string.Compare(seachString, std.Name, System.StringComparison.InvariantCultureIgnoreCase) == 0; 87 | } 88 | 89 | public bool IsItemMatchingSearchString(object sender, object item, string searchString) 90 | { 91 | if (!(item is Person person)) 92 | { 93 | return false; 94 | } 95 | 96 | if (string.IsNullOrEmpty(searchString)) 97 | { 98 | return true; 99 | } 100 | 101 | return person.Name?.ToLower()?.Contains(searchString?.ToLower()) == true 102 | || person.Company.ToString().ToLower()?.Contains(searchString?.ToLower()) == true 103 | || person.Zip?.ToLower()?.Contains(searchString?.ToLower()) == true; 104 | } 105 | } 106 | ``` 107 | -------------------------------------------------------------------------------- /Docs/Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/Logo.png -------------------------------------------------------------------------------- /Docs/MultiSelectCombobox_Design_Doc_v1.md: -------------------------------------------------------------------------------- 1 | # MultiSelectCombobox 2 | ## Content: 3 | * [Overview](#overview) 4 | * [Design](#design-overview) 5 | * [Dependency Properties](#dependency-properties) 6 | * [Explaining Demo Code](Demo_App_Code_v1.md) 7 | 8 | ## Overview 9 | WPF has ListBox control which lets user select more than one item. However, ListBox control UI doesn't have in-built support for searching/filtering. Developers have to do work around to provision one. Moreover, lot of mouse interaction is required. Yes, you may be able to do all completely using keyboard. But, not most efficient way of doing. On the other hand, Combobox has a very good UI which supports searching and filtering. However, it doesn't support multiple selection. 10 | 11 | What if we can combine behavior of ListBox and goodness of Combobox UI? MultiSelectCombobox exactly does the same thing. It provides functionality of searching/filtering with multiple selection. MultiSelectCombobox tries to mimic UI behavior of ComboBox. 12 | 13 | ![](App2.0_2.gif) 14 | 15 | *** 16 | 17 | ## Design Overview 18 | MultiSelectCombobox is composed of RichTextBox, Popup and ListBox. Text entered in RichTextBox is monitored and manipulated. On key press, popup box will show up and display items from source collection matching search criteria. If there is no matching item in collection, it won't show up. If it finds suitable item from source collection, it will replace entered text with source collection item. Selected item is shown as TextBlock - Inline UI element. 19 | 20 | Individual control placement and behavior can be changed with Control template. Template parts are defined as following: 21 | ```csharp 22 | [TemplatePart(Name = "rtxt", Type = typeof(RichTextBox))] 23 | [TemplatePart(Name = "popup", Type = typeof(Popup))] 24 | [TemplatePart(Name = "lstSuggestion", Type = typeof(ListBox))] 25 | public sealed partial class MultiSelectCombobox : Control 26 | { 27 | ``` 28 | 29 | ### Dependency Properties 30 | Control is designed to expose minimal properties which are required to make it work. 31 | 32 | **1. `ItemSource (IEnumerable)`** - Source collection should be bound to this property. It support collection of as simple type as string to complex type/entities. 33 | 34 | **2. `SelectedItems (IList)`** - This property will provide collection of items selected by user. 35 | 36 | **3. `ItemSeparator (char)`** - default value is ';'. In control, items are separated with ItemSeparator char. This is important if items contain spaces. Separator should be chosen carefully. Moreover, to indicate end of item while entering or forcing control to create new item based on current entered text, this character it used. Also, if user enters text which does not match any item provided in collection or LookUpContract does not support creation of object from given text, user entered text will be removed from control UI. Support for creation of new item is discussed later in this document. 37 | 38 | **4. `DisplayMemberPath (string)`** - If ItemSource collection is of complex type, developer may need to override ToString() method of type or else can define DisplayMemberPath property. Default value is string.Empty. 39 | 40 | **5. `LookUpContract (ILookUpContract)`** - This property is used to customize searching/filtering behavior of the control. Control provides default implementation which works for most users. However, in case of Complex type and/or custom searching/filtering behavior, user can provide implementation and change control behavior. 41 | 42 | #### Explaining LookUpContract (`ILookUpContract`) for advance scenarios 43 | 44 | Default search/filtering work on string.StartsWith & string.Equals respectively. For any given item, if DisplayMemberPath is not set, item.ToString() value is sent to filtering mechanism. If DisplayMemberPath is provided, path value is fetched through item property reflection and sent to filter mechanism. This works for most of the user. 45 | 46 | However, if user needs to customize these setting/filtering mechanism, he/she can provide implementation of this interface and bind to LookUpContract property. Control will respect newly bound implementation. 47 | 48 | **ILookUpContract.cs** 49 | 50 | ```csharp 51 | public interface ILookUpContract 52 | { 53 | // Whether contract supports creation of new object from user entered text 54 | bool SupportsNewObjectCreation { get; } 55 | 56 | // Method to check if item matches searchString 57 | bool IsItemMatchingSearchString(object sender, object item, string searchString); 58 | 59 | // Checks if item matches searchString or not 60 | bool IsItemEqualToString(object sender, object item, string seachString); 61 | 62 | // Creates object from provided string 63 | // This method need to be implemented only when SupportsNewObjectCreation is set to true 64 | object CreateObject(object sender, string searchString); 65 | } 66 | ``` 67 | 68 | * **`IsItemMatchingSearchString`** - This function is called to filter suggestion items in drop-down list. User entered text is passed as parameter to this function. Return true if item should be displayed in suggestion drop-down for given text. Otherwise return false. 69 | 70 | * **`IsItemEqualToString`** - This function is used to find exact item from collection based on user entered text. 71 | 72 | * **`CreateObject`** - This function should only be implemented if SupportsNewObjectCreation is set to true. This function is called to create new object based on provided text. For example, in [AdvanceLookUpContract](https://github.com/nilayjoshi89/BlackPearl/blob/master/BlackPearl.Controls.Demo/AdvanceLookUpContract.cs) implementation, we can create complex object by entering comma separated value in control ending with ItemSeparator (as shown in above GIF). This is just a sample implementation. You can define your own format/parsing mechanism. 73 | 74 | * **`SupportsNewObjectCreation`** - If this property is set to false, control will not allow user to select item other than provided collection (ItemSource). If this property is set to true, control will allow creation of new object. This is useful when control should let user add new object. Also eliminates need to create separate TextBox(es) and button to add new item in existing SelectedItems/ItemSource. 75 | 76 | * **[DefaultLookUpContract](https://github.com/nilayjoshi89/BlackPearl/blob/master/BlackPearl.Controls.Library/Control/ILookUpContract.cs)** - If no new implementation is provided to control, this DefaultLookUpContract implementation is used. This contract uses string.StartsWith for searching and string.Equals for comparison. Both comparison is invariant of culture and case. -------------------------------------------------------------------------------- /Docs/Version_History.md: -------------------------------------------------------------------------------- 1 | ### Version 2.0.3 2 | 3 | **Release type:** Bug fix release 4 | 5 | **Release Notes:** This release contains bug fixes. Please refer to issues list for detail. 6 | 7 | **Issues/Enhancements:** 8 | * [Popup placement issue](https://github.com/nilayjoshi89/BlackPearl/issues/22) 9 | * [Copy/Paste issue](https://github.com/nilayjoshi89/BlackPearl/issues/23) 10 | 11 | ### Version 2.0.2 12 | 13 | **Release type:** Bug fix release 14 | 15 | **Release Notes:** This release contains bug fixes. Please refer to issues list for detail. 16 | 17 | **Issues/Enhancements:** 18 | * [Setting SelectedItems after load changes Popup visibility](https://github.com/nilayjoshi89/BlackPearl/issues/20) 19 | 20 | ### Version 2.0.1 21 | 22 | **Release type:** Bug fix release 23 | 24 | **Release Notes:** This release contains bug fixes. Please refer to issues list for detail. 25 | 26 | **Issues/Enhancements:** 27 | * [NullRefException if selected items is set to null](https://github.com/nilayjoshi89/BlackPearl/issues/18) 28 | 29 | ### Version 2.0.0 30 | 31 | **Release type:** Enhancement release 32 | 33 | **Release Notes:** This release contains feature enhancement and performance fixes. Please refer to issues list for detail. This release has breaking changes. `SelectedItemTextBlockStyleProperty` and `SearchTextStyleProperty` dependency properties are removed as it's custom control now and can be modified using ControlTemplate. 34 | 35 | **Issues/Enhancements:** 36 | * [Convert UserControl to CustomControl](https://github.com/nilayjoshi89/BlackPearl/issues/14) 37 | * [Add support for .NET core 3.1](https://github.com/nilayjoshi89/BlackPearl/issues/12) 38 | 39 | ### Version 1.0.0 40 | **Release type:** Initial release -------------------------------------------------------------------------------- /Docs/copy_paste-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/copy_paste-min.gif -------------------------------------------------------------------------------- /Docs/drag_drop-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/drag_drop-min.gif -------------------------------------------------------------------------------- /Docs/drag_drop_2-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nilayjoshi89/BlackPearl/c76c51adb9bbee1c6ef2966209bba710b53fad89/Docs/drag_drop_2-min.gif -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nilay Joshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlackPearl WPF Control Library 2 | [![Build Status](https://nilayjoshi89.visualstudio.com/BlackPearl%202.0/_apis/build/status/nilayjoshi89.BlackPearl?branchName=master)](https://nilayjoshi89.visualstudio.com/BlackPearl%202.0/_build/latest?definitionId=5&branchName=master) 3 | [![NuGet BlackPearl](https://img.shields.io/nuget/v/BlackPearl.Controls.Library.svg?label=NuGet%20BlackPearl.Controls.Library)](https://www.nuget.org/packages/BlackPearl.Controls.Library/) 4 | [![NuGet BlackPearl](https://img.shields.io/nuget/dt/BlackPearl.Controls.Library.svg?style=flat-square)](https://www.nuget.org/packages/BlackPearl.Controls.Library/) 5 | ![GitHub](https://img.shields.io/github/license/nilayjoshi89/BlackPearl) 6 | [![Gitter](https://badges.gitter.im/BlackPearl-WPF-Control-Library/community.svg)](https://gitter.im/BlackPearl-WPF-Control-Library/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 7 | *** 8 | 9 | # About repository 10 | This repository is created to put different basic WPF Custom-control/User-controls under same roof. These controls solve basic and frequently faced issue by developers. As of now, this contains only one custom-control **MultiSelectCombobox**. In future, I'm planning to add more. 11 | 12 | [BlackPearl](https://github.com/nilayjoshi89/BlackPearl) is licensed under [MIT license](https://github.com/nilayjoshi89/BlackPearl/blob/master/LICENSE). 13 | *** 14 | 15 | ### Content: 16 | * [MultiSelectCombobox (v2.0.0)](#multiselectcombobox) 17 | * [Overview](#overview) 18 | * [Feature](#feature) 19 | * [Design Document](Docs/MultiSelectCombobox_Design_Doc_v1.md) 20 | * [Explaining Demo Code](Docs/Demo_App_Code_v1.md) 21 | * [Version History](Docs/Version_History.md) 22 | 23 | *** 24 | 25 | # MultiSelectCombobox 26 | 27 | ## Overview 28 | WPF has ListBox control which lets user select more than one item. However, ListBox control UI doesn't have in-built support for searching/filtering. Developers have to do work around to provision one. Moreover, lot of mouse interaction is required. Yes, you may be able to do all completely using keyboard. But, not most efficient way of doing. On the other hand, Combobox has a very good UI which supports searching and filtering. However, it doesn't support multiple selection. 29 | 30 | What if we can combine behavior of ListBox and goodness of Combobox UI? MultiSelectCombobox exactly does the same thing. It provides functionality of searching/filtering with multiple selection. MultiSelectCombobox tries to mimic UI behavior of ComboBox. 31 | 32 | ![](Docs/App2.0_Screenshot_1.png) 33 | 34 | ![](Docs/App2.0_2.gif) 35 | 36 | *** 37 | ## Feature 38 | * In built support for searching and filtering 39 | * Extensible to support custom searching and filtering for Complex data type 40 | * Ability to create and add new item which is not part of source collection (through LookUpContract for complex types) 41 | * Easy to use! 42 | -------------------------------------------------------------------------------- /Source/Common/BlackPearl.Controls.Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(TargetFrameworkVersion) 4 | 5 | 6 | -------------------------------------------------------------------------------- /Source/Common/Contract/ILookUpContract.cs: -------------------------------------------------------------------------------- 1 | namespace BlackPearl.Controls.Contract 2 | { 3 | /// 4 | /// Look-up contract for custom search behavior 5 | /// 6 | public interface ILookUpContract 7 | { 8 | /// 9 | /// Whether contract supports creation of new object from user entered text 10 | /// 11 | bool SupportsNewObjectCreation { get; } 12 | /// 13 | /// Method to check if item matches searchString 14 | /// 15 | /// control 16 | /// item to check 17 | /// search string 18 | /// true/false 19 | bool IsItemMatchingSearchString(object sender, object item, string searchString); 20 | /// 21 | /// Checks if item matches searchString or not 22 | /// 23 | /// control 24 | /// item to check 25 | /// search string 26 | /// true if matches otherwise false 27 | bool IsItemEqualToString(object sender, object item, string seachString); 28 | /// 29 | /// Creates object from provided string 30 | /// This method need to be implemented only when SupportsNewObjectCreation is set to true 31 | /// 32 | /// control 33 | /// text from which object need to be created 34 | /// newly created object 35 | object CreateObject(object sender, string searchString); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /Source/Common/Extension/GeneralExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Linq; 3 | using System.Text; 4 | 5 | namespace BlackPearl.Controls.Extension 6 | { 7 | public static class GeneralExtension 8 | { 9 | public static string RemoveDiacritics(this string text) => 10 | //"héllo" becomes "hello", which in turn becomes "hello". 11 | string.Concat(text.Normalize(NormalizationForm.FormD).Where(ch => CharUnicodeInfo.GetUnicodeCategory(ch) != UnicodeCategory.NonSpacingMark)).Normalize(NormalizationForm.FormC); 12 | } 13 | } -------------------------------------------------------------------------------- /Source/Common/Extension/ReflectionExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace BlackPearl.Controls.Extension 4 | { 5 | public static class ReflectionExtension 6 | { 7 | public static object GetPropertyValue(this object obj, string path) 8 | { 9 | if (string.IsNullOrEmpty(path) || obj == null) 10 | { 11 | return obj; 12 | } 13 | 14 | int dotIndex = path.IndexOf('.'); 15 | if (dotIndex < 0) 16 | { 17 | return GetValue(obj, path); 18 | } 19 | 20 | obj = GetValue(obj, path.Substring(0, dotIndex + 1)); 21 | path = path.Remove(0, dotIndex); 22 | 23 | return obj.GetPropertyValue(path); 24 | } 25 | 26 | private static object GetValue(object obj, string propertyName) 27 | { 28 | PropertyInfo propInfo = obj.GetType().GetProperty(propertyName); 29 | if (propInfo == null) 30 | { 31 | return null; 32 | } 33 | 34 | return propInfo.GetValue(obj); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Source/CoreLibrary/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | [assembly: ThemeInfo( 4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 5 | //(used if a resource is not found in the page, 6 | // or application resource dictionaries) 7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 8 | //(used if a resource is not found in the page, 9 | // app, or any theme specific resource dictionaries) 10 | )] 11 | -------------------------------------------------------------------------------- /Source/CoreLibrary/Behavior/ListBoxItemBehavior.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace BlackPearl.Controls.CoreLibrary.Behavior 5 | { 6 | //References - http://stackoverflow.com/questions/1114092/listbox-scrollbar-doesnt-follow-selected-item-with-icollectionview 7 | // - https://www.codeproject.com/Articles/28959/Introduction-to-Attached-Behaviors-in-WPF 8 | public static class ListBoxItemBehavior 9 | { 10 | public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty = DependencyProperty.RegisterAttached( 11 | name: "IsBroughtIntoViewWhenSelected", 12 | propertyType: typeof(bool), 13 | ownerType: typeof(ListBoxItemBehavior), 14 | defaultMetadata: new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged)); 15 | public static bool GetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem) => (bool)listBoxItem.GetValue(IsBroughtIntoViewWhenSelectedProperty); 16 | public static void SetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem, bool value) => listBoxItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value); 17 | 18 | private static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e) 19 | { 20 | if (!(depObj is ListBoxItem item) 21 | || !(e.NewValue is bool broughtIntoViewWhenSelected)) 22 | { 23 | return; 24 | } 25 | 26 | if (broughtIntoViewWhenSelected) 27 | { 28 | item.Selected += OnListBoxItemSelected; 29 | } 30 | else 31 | { 32 | item.Selected -= OnListBoxItemSelected; 33 | } 34 | } 35 | private static void OnListBoxItemSelected(object sender, RoutedEventArgs e) 36 | { 37 | // Only react to the Selected event raised by the ListBoxItem 38 | // whose IsSelected property was modified. Ignore all ancestors 39 | // who are merely reporting that a descendant's Selected fired. 40 | if (!ReferenceEquals(sender, e.OriginalSource)) 41 | { 42 | return; 43 | } 44 | 45 | if (e.OriginalSource is ListBoxItem item) 46 | { 47 | item.BringIntoView(); 48 | } 49 | } 50 | } 51 | 52 | public static class ListBoxAttachedProperties 53 | { 54 | public static readonly DependencyProperty SelectionStartIndexProperty = DependencyProperty.RegisterAttached( 55 | name: "SelectionStartIndex", 56 | propertyType: typeof(int), 57 | ownerType: typeof(ListBoxItemBehavior), 58 | defaultMetadata: new UIPropertyMetadata(-1)); 59 | public static int GetSelectionStartIndex(ListBox listBox) => (int)listBox.GetValue(SelectionStartIndexProperty); 60 | public static void SetSelectionStartIndex(ListBox listBox, int value) => listBox.SetValue(SelectionStartIndexProperty, value); 61 | 62 | public static readonly DependencyProperty SelectionEndIndexProperty = DependencyProperty.RegisterAttached( 63 | name: "SelectionEndIndex", 64 | propertyType: typeof(int), 65 | ownerType: typeof(ListBoxItemBehavior), 66 | defaultMetadata: new UIPropertyMetadata(-1)); 67 | public static int GetSelectionEndIndex(ListBox listBox) => (int)listBox.GetValue(SelectionEndIndexProperty); 68 | public static void SetSelectioEndtIndex(ListBox listBox, int value) => listBox.SetValue(SelectionEndIndexProperty, value); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Source/CoreLibrary/BlackPearl.Controls.CoreLibrary.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(TargetFrameworkVersion) 4 | true 5 | WPF control library 6 | nilayjoshi89@gmail.com 7 | Copyright 2023 8 | MIT 9 | WPF MultiSelectCombobox ComboBox MultiSelectDropDown 10 | https://github.com/nilayjoshi89/BlackPearl 11 | false 12 | false 13 | Control Library 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Source/CoreLibrary/BlackPearl.Controls.CoreLibrary.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BlackPearl.Controls.Library 5 | 2.0.4.0 6 | WPF control library 7 | nilayjoshi89@gmail.com 8 | WPF control library 9 | https://github.com/nilayjoshi89/BlackPearl 10 | Added Drag-Drop support 11 | Copyright 2021 12 | WPF MultiSelectCombobox ComboBox MultiSelectDropDown 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /Source/CoreLibrary/Controls/MultiSelectCombobox/BasicStructure.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Controls.Primitives; 6 | using System.Windows.Input; 7 | 8 | using BlackPearl.Controls.Contract; 9 | 10 | namespace BlackPearl.Controls.CoreLibrary 11 | { 12 | [TemplatePart(Name = "rtxt", Type = typeof(RichTextBox))] 13 | [TemplatePart(Name = "popup", Type = typeof(Popup))] 14 | [TemplatePart(Name = "lstSuggestion", Type = typeof(ListBox))] 15 | public sealed partial class MultiSelectCombobox : Control 16 | { 17 | #region Constructor 18 | static MultiSelectCombobox() 19 | { 20 | DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiSelectCombobox), new FrameworkPropertyMetadata(typeof(MultiSelectCombobox))); 21 | } 22 | public MultiSelectCombobox() 23 | { 24 | PreviewKeyDown += MultiSelectCombobox_PreviewKeyDown; 25 | LostFocus += MultiSelectCombobox_LostFocus; 26 | } 27 | 28 | #endregion 29 | 30 | #region Template Parts 31 | public override void OnApplyTemplate() 32 | { 33 | base.OnApplyTemplate(); 34 | RichTextBoxElement = GetTemplateChild("rtxt") as RichTextBox; 35 | PopupElement = GetTemplateChild("popup") as Popup; 36 | SuggestionElement = GetTemplateChild("lstSuggestion") as ListBox; 37 | } 38 | 39 | private RichTextBox richTextBoxElement; 40 | private RichTextBox RichTextBoxElement 41 | { 42 | get => richTextBoxElement; 43 | set 44 | { 45 | if (richTextBoxElement != null) 46 | { 47 | richTextBoxElement.TextChanged -= RichTextBoxElement_TextChanged; 48 | richTextBoxElement.SizeChanged -= RichTextBoxElement_SizeChanged; 49 | DataObject.RemovePastingHandler(richTextBoxElement, PasteHandler); 50 | DataObject.RemoveCopyingHandler(richTextBoxElement, OnSelectionStartDrag); 51 | richTextBoxElement.RemoveHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(SetClipboardTextWithCommandCancelled)); 52 | richTextBoxElement.DragEnter -= OnDragEnter; 53 | richTextBoxElement.Drop -= OnDragDrop; 54 | } 55 | 56 | richTextBoxElement = value; 57 | 58 | if (richTextBoxElement != null) 59 | { 60 | richTextBoxElement.SetParagraphAsFirstBlock(); 61 | 62 | if (SelectedItems != null) 63 | { 64 | //Add all selected items 65 | foreach (object item in SelectedItems) 66 | { 67 | richTextBoxElement.AddToParagraph(item, CreateInlineUIElement); 68 | } 69 | } 70 | 71 | richTextBoxElement.TextChanged += RichTextBoxElement_TextChanged; 72 | richTextBoxElement.SizeChanged += RichTextBoxElement_SizeChanged; 73 | DataObject.AddPastingHandler(richTextBoxElement, PasteHandler); 74 | DataObject.AddCopyingHandler(richTextBoxElement, OnSelectionStartDrag); 75 | richTextBoxElement.AddHandler(CommandManager.PreviewExecutedEvent, new ExecutedRoutedEventHandler(SetClipboardTextWithCommandCancelled)); 76 | richTextBoxElement.DragEnter += OnDragEnter; 77 | richTextBoxElement.Drop += OnDragDrop; 78 | richTextBoxElement.AllowDrop = true; 79 | } 80 | } 81 | } 82 | 83 | private Popup PopupElement { get; set; } 84 | 85 | private ListBox suggestionElement; 86 | private ListBox SuggestionElement 87 | { 88 | get => suggestionElement; 89 | set 90 | { 91 | if (suggestionElement != null) 92 | { 93 | suggestionElement.PreviewMouseUp -= SuggestionDropdown_PreviewMouseUp; 94 | suggestionElement.PreviewKeyUp -= SuggestionElement_PreviewKeyUp; 95 | suggestionElement.PreviewMouseDown -= SuggestionDropdown_PreviewMouseDown; 96 | } 97 | 98 | suggestionElement = value; 99 | suggestionElement.DisplayMemberPath = DisplayMemberPath; 100 | suggestionElement.ItemsSource = ItemSource; 101 | 102 | if (suggestionElement != null) 103 | { 104 | suggestionElement.PreviewMouseUp += SuggestionDropdown_PreviewMouseUp; 105 | suggestionElement.PreviewKeyUp += SuggestionElement_PreviewKeyUp; 106 | suggestionElement.PreviewMouseDown += SuggestionDropdown_PreviewMouseDown; 107 | } 108 | } 109 | } 110 | 111 | #endregion 112 | 113 | #region Properties 114 | /// 115 | /// Item source 116 | /// 117 | public static readonly DependencyProperty ItemSourceProperty = 118 | DependencyProperty.Register(nameof(ItemSource), typeof(IEnumerable), typeof(MultiSelectCombobox), new PropertyMetadata(ItemSourcePropertyChanged)); 119 | public IEnumerable ItemSource 120 | { 121 | get => (IEnumerable)GetValue(ItemSourceProperty); 122 | set => SetValue(ItemSourceProperty, value); 123 | } 124 | 125 | /// 126 | /// List of selected items 127 | /// 128 | public static readonly DependencyProperty SelectedItemsProperty = 129 | DependencyProperty.Register(nameof(SelectedItems), typeof(IList), typeof(MultiSelectCombobox), new PropertyMetadata(SelectedItemsChanged)); 130 | public IList SelectedItems 131 | { 132 | get => (IList)GetValue(SelectedItemsProperty); 133 | set => SetValue(SelectedItemsProperty, value); 134 | } 135 | 136 | /// 137 | /// Char value that separates two selected items. Default value is ';' 138 | /// 139 | public static readonly DependencyProperty ItemSeparatorProperty = 140 | DependencyProperty.Register(nameof(ItemSeparator), typeof(char), typeof(MultiSelectCombobox), new PropertyMetadata(';')); 141 | public char ItemSeparator 142 | { 143 | get => (char)GetValue(ItemSeparatorProperty); 144 | set => SetValue(ItemSeparatorProperty, value); 145 | } 146 | 147 | /// 148 | /// Array of additional char value that separates two selected items. Default value is null 149 | /// 150 | public static readonly DependencyProperty AdditionalItemSeparatorsProperty = 151 | DependencyProperty.Register(nameof(AdditionalItemSeparators), typeof(char[]), typeof(MultiSelectCombobox), new PropertyMetadata(System.Array.Empty())); 152 | public char[] AdditionalItemSeparators 153 | { 154 | get => (char[])GetValue(AdditionalItemSeparatorsProperty); 155 | set => SetValue(AdditionalItemSeparatorsProperty, value); 156 | } 157 | 158 | /// 159 | /// Display member path - for complex object, we can set this to show value on given path 160 | /// 161 | public static readonly DependencyProperty DisplayMemberPathProperty = 162 | DependencyProperty.Register(nameof(DisplayMemberPath), typeof(string), typeof(MultiSelectCombobox), new PropertyMetadata(DisplayMemberPathChanged)); 163 | public string DisplayMemberPath 164 | { 165 | get => (string)GetValue(DisplayMemberPathProperty); 166 | set => SetValue(DisplayMemberPathProperty, value); 167 | } 168 | 169 | /// 170 | /// ILookUpContract - implementation for custom behavior of Look-up and create. 171 | /// If not set, default behavior will be set. 172 | /// 173 | public static readonly DependencyProperty LookUpContractProperty = 174 | DependencyProperty.Register(nameof(LookUpContract), typeof(ILookUpContract), typeof(MultiSelectCombobox), new PropertyMetadata(new DefaultLookUpContract())); 175 | public ILookUpContract LookUpContract 176 | { 177 | get => (ILookUpContract)GetValue(LookUpContractProperty); 178 | set => SetValue(LookUpContractProperty, value); 179 | } 180 | #endregion 181 | 182 | #region Property changed callback 183 | /// 184 | /// When selected item property is changed 185 | /// 186 | /// control 187 | /// arg 188 | private static void SelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 189 | { 190 | if (!(d is MultiSelectCombobox multiChoiceControl 191 | && e.NewValue is IList selectedItems && selectedItems != null)) 192 | { 193 | return; 194 | } 195 | 196 | try 197 | { 198 | //Unsubscribe handlers first 199 | if (!multiChoiceControl.UnsubscribeHandler() 200 | || multiChoiceControl?.RichTextBoxElement == null) 201 | { 202 | //Failed to unsubscribe, return 203 | return; 204 | } 205 | 206 | foreach (var textblock in multiChoiceControl?.RichTextBoxElement?.GetParagraph()?.Inlines?.Select(i => i.GetTextBlock())?.Where(i => i != null)) 207 | { 208 | textblock.Unloaded -= multiChoiceControl.Tb_Unloaded; 209 | } 210 | 211 | //Clear everything in RichTextBox 212 | multiChoiceControl.RichTextBoxElement?.ClearParagraph(); 213 | 214 | //Add all selected items 215 | foreach (object item in selectedItems) 216 | { 217 | multiChoiceControl?.RichTextBoxElement?.AddToParagraph(item, multiChoiceControl.CreateInlineUIElement); 218 | } 219 | 220 | multiChoiceControl.RaiseSelectionChangedEvent(e.OldValue as IList ?? new ArrayList(0), e.NewValue as IList ?? new ArrayList(0)); 221 | } 222 | finally 223 | { 224 | multiChoiceControl.SubsribeHandler(); 225 | } 226 | } 227 | /// 228 | /// When ItemSource property is changed 229 | /// 230 | /// control 231 | /// arguments 232 | private static void ItemSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 233 | { 234 | if (!(d is MultiSelectCombobox multiChoiceControl)) 235 | { 236 | return; 237 | } 238 | 239 | if (multiChoiceControl.SuggestionElement == null) 240 | { 241 | return; 242 | } 243 | 244 | multiChoiceControl.SuggestionElement.ItemsSource = (e.NewValue as IEnumerable)?.Cast(); 245 | } 246 | 247 | /// 248 | /// Display member path change handler 249 | /// 250 | /// 251 | /// 252 | private static void DisplayMemberPathChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 253 | { 254 | if (!(d is MultiSelectCombobox msc) 255 | || msc.SuggestionElement == null) 256 | { 257 | return; 258 | } 259 | 260 | msc.SuggestionElement.DisplayMemberPath = e.NewValue?.ToString(); 261 | } 262 | #endregion 263 | 264 | #region Routed Event 265 | public static readonly RoutedEvent SelectionChangedEvent = EventManager.RegisterRoutedEvent( 266 | name: nameof(SelectionChanged), 267 | routingStrategy: RoutingStrategy.Bubble, 268 | handlerType: typeof(SelectionChangedEventHandler), ownerType: 269 | typeof(MultiSelectCombobox)); 270 | public event SelectionChangedEventHandler SelectionChanged 271 | { 272 | add { AddHandler(SelectionChangedEvent, value); } 273 | remove { RemoveHandler(SelectionChangedEvent, value); } 274 | } 275 | 276 | /// 277 | /// Raise SelectionChanged event 278 | /// 279 | /// removed items 280 | /// added items 281 | private void RaiseSelectionChangedEvent(IList removedItems, IList addedItems) => RaiseEvent(new SelectionChangedEventArgs(SelectionChangedEvent, removedItems, addedItems)); 282 | #endregion 283 | } 284 | } -------------------------------------------------------------------------------- /Source/CoreLibrary/Controls/MultiSelectCombobox/CustomTypes.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Controls; 3 | 4 | namespace BlackPearl.Controls.CoreLibrary 5 | { 6 | public class MultiSelectCombobox_CustomRichTextBox : RichTextBox 7 | { 8 | //disable default OnDrop event witch give " " strings... 9 | protected override void OnDrop(DragEventArgs e) { } 10 | } 11 | } -------------------------------------------------------------------------------- /Source/CoreLibrary/Controls/MultiSelectCombobox/EntensionMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Controls.Primitives; 8 | using System.Windows.Documents; 9 | 10 | using BlackPearl.Controls.Contract; 11 | using BlackPearl.Controls.CoreLibrary.Behavior; 12 | 13 | namespace BlackPearl.Controls.CoreLibrary 14 | { 15 | internal static class EntensionMethods 16 | { 17 | #region ListBox 18 | public static void ClearSelection(this ListBox suggestionList, Func precondition = null) 19 | { 20 | if (precondition != null 21 | && !precondition()) 22 | { 23 | return; 24 | } 25 | 26 | suggestionList?.SelectedItems?.Clear(); 27 | } 28 | public static IEnumerable GetItemSource(this ListBox suggestionList) => suggestionList?.ItemsSource?.Cast(); 29 | public static void SelectNextItem(this ListBox suggestionList) => suggestionList.SingleItemSelection(1); 30 | public static void SelectPreviousItem(this ListBox suggestionList) => suggestionList.SingleItemSelection(-1); 31 | public static void SelectMultipleNextItem(this ListBox suggestionList) => suggestionList.SelectMultipleItem(1); 32 | public static void SelectMultiplePreviousItem(this ListBox suggestionList) => suggestionList.SelectMultipleItem(-1); 33 | public static int GetSelectionStart(this ListBox suggestionList) 34 | { 35 | if (suggestionList == null) 36 | { 37 | return -1; 38 | } 39 | 40 | return ListBoxAttachedProperties.GetSelectionStartIndex(suggestionList); 41 | } 42 | public static void SetSelectionStart(this ListBox suggestionList, int index) 43 | { 44 | if (suggestionList == null) 45 | { 46 | return; 47 | } 48 | 49 | ListBoxAttachedProperties.SetSelectionStartIndex(suggestionList, index); 50 | } 51 | public static int GetSelectionEnd(this ListBox suggestionList) 52 | { 53 | if (suggestionList == null) 54 | { 55 | return -1; 56 | } 57 | 58 | return ListBoxAttachedProperties.GetSelectionEndIndex(suggestionList); 59 | } 60 | public static void SetSelectionEnd(this ListBox suggestionList, int index) 61 | { 62 | if (suggestionList == null) 63 | { 64 | return; 65 | } 66 | 67 | ListBoxAttachedProperties.SetSelectioEndtIndex(suggestionList, index); 68 | } 69 | 70 | public static void CleanOperation(this ListBox suggestionList, SuggestionCleanupOperation operation, IEnumerable goldenItemSource) 71 | { 72 | if ((operation & SuggestionCleanupOperation.ClearSelection) == SuggestionCleanupOperation.ClearSelection) 73 | { 74 | suggestionList.ClearSelection(); 75 | } 76 | 77 | if ((operation & SuggestionCleanupOperation.ResetIndex) == SuggestionCleanupOperation.ResetIndex) 78 | { 79 | suggestionList.SetSelectionStart(-1); 80 | suggestionList.SetSelectionEnd(-1); 81 | } 82 | 83 | if ((operation & SuggestionCleanupOperation.ResetItemSource) == SuggestionCleanupOperation.ResetItemSource) 84 | { 85 | suggestionList.ItemsSource = goldenItemSource; 86 | } 87 | } 88 | 89 | private static void SingleItemSelection(this ListBox suggestionList, int delta) 90 | { 91 | if (suggestionList == null 92 | || (suggestionList.SelectedIndex + delta) < 0) 93 | { 94 | return; 95 | } 96 | 97 | suggestionList.SelectedIndex += delta; 98 | 99 | suggestionList.SetSelectionStart(-1); 100 | suggestionList.SetSelectionEnd(-1); 101 | } 102 | private static void SelectMultipleItem(this ListBox suggestionList, int delta) 103 | { 104 | if (suggestionList == null) 105 | { 106 | return; 107 | } 108 | 109 | ItemCollection suggestionItemSource = suggestionList.Items; 110 | int selectionStart = suggestionList.GetSelectionStart(); 111 | int selectionEnd = suggestionList.GetSelectionEnd(); 112 | int totalItemsCount = suggestionItemSource.Count; 113 | 114 | //If its first time - Start of selection 115 | if (selectionStart == -1 || selectionEnd == -1) 116 | { 117 | selectionStart = suggestionList.SelectedIndex; 118 | selectionEnd = suggestionList.SelectedIndex + delta; 119 | selectionEnd = (selectionEnd < 0) 120 | ? 0 121 | : (selectionEnd >= totalItemsCount) 122 | ? totalItemsCount - 1 123 | : selectionEnd; 124 | 125 | suggestionList.SetSelectionStart(selectionStart); 126 | suggestionList.SetSelectionEnd(selectionEnd); 127 | 128 | //Add current item to selected items list 129 | suggestionList.SelectedItems?.Add(suggestionItemSource[selectionEnd]); 130 | return; 131 | } 132 | 133 | int newIndex = selectionEnd + delta; 134 | newIndex = (newIndex < 0) 135 | ? 0 136 | : (newIndex >= totalItemsCount) 137 | ? totalItemsCount - 1 138 | : newIndex; 139 | 140 | //If its boundary, return 141 | if (selectionEnd == newIndex) 142 | { 143 | return; 144 | } 145 | 146 | //If selection is shrinking then remove previous selected element 147 | if ((selectionStart > selectionEnd && newIndex > selectionEnd) 148 | || (selectionStart < selectionEnd && newIndex < selectionEnd)) 149 | { 150 | suggestionList.SelectedItems?.Remove(suggestionItemSource[selectionEnd]); 151 | suggestionList.SetSelectionEnd(newIndex); 152 | return; 153 | } 154 | 155 | //Otherwise, selection is growing, add current element to selected items list 156 | suggestionList.SetSelectionEnd(newIndex); 157 | suggestionList.SelectedItems?.Add(suggestionItemSource[newIndex]); 158 | } 159 | 160 | [Flags] 161 | internal enum SuggestionCleanupOperation 162 | { 163 | None = 0, 164 | ResetIndex = 1, 165 | ClearSelection = 2, 166 | ResetItemSource = 4 167 | }; 168 | #endregion 169 | 170 | #region RichTextBox 171 | public static string GetCurrentText(this RichTextBox richTextBox) => GetCurrentRunBlock(richTextBox)?.Text; 172 | public static void ResetCurrentText(this RichTextBox richTextBox) 173 | { 174 | Run runElement = GetCurrentRunBlock(richTextBox); 175 | if (runElement == null) 176 | { 177 | return; 178 | } 179 | 180 | runElement.Text = string.Empty; 181 | } 182 | public static void RemoveRunBlocks(this RichTextBox richTextBox) 183 | { 184 | Paragraph paragraph = richTextBox?.GetParagraph(); 185 | if (paragraph == null) 186 | { 187 | return; 188 | } 189 | 190 | var runTags = paragraph.Inlines?.Where(r => r is Run)?.ToList(); 191 | if (runTags?.Any() != true) 192 | { 193 | return; 194 | } 195 | 196 | for (int i = 0; i < runTags.Count; i++) 197 | { 198 | paragraph?.Inlines?.Remove(runTags[i]); 199 | } 200 | } 201 | public static void TryFocus(this RichTextBox richTextBox) 202 | { 203 | try 204 | { 205 | if (richTextBox?.Focusable == true) 206 | { 207 | richTextBox.Focus(); 208 | } 209 | } 210 | catch { } 211 | } 212 | public static void SetParagraphAsFirstBlock(this RichTextBox richTextBox) 213 | { 214 | try 215 | { 216 | if (richTextBox.GetParagraph() != null) 217 | { 218 | return; 219 | } 220 | 221 | var paragraph = new Paragraph() { Style = new Style() }; 222 | richTextBox.Document.Blocks.Clear(); 223 | richTextBox.Document.Blocks.Add(paragraph); 224 | } 225 | catch { } 226 | } 227 | 228 | 229 | public static void AddToParagraph(this RichTextBox richTextBox, object itemToAdd, Func createInlineElementFunct) 230 | { 231 | try 232 | { 233 | richTextBox?.SetParagraphAsFirstBlock(); 234 | Paragraph paragraph = richTextBox?.GetParagraph(); 235 | if (paragraph == null) 236 | { 237 | return; 238 | } 239 | 240 | Inline elementToAdd = createInlineElementFunct(itemToAdd); 241 | if (paragraph.Inlines.FirstInline == null) 242 | { 243 | //First element to insert 244 | paragraph.Inlines.Add(elementToAdd); 245 | richTextBox.CaretPosition = richTextBox.CaretPosition.DocumentEnd; 246 | return; 247 | } 248 | 249 | if (richTextBox.CaretPosition.GetAdjacentElement(LogicalDirection.Forward) is Inline inlineToInsertBefore) 250 | { 251 | paragraph.Inlines.InsertBefore(inlineToInsertBefore, elementToAdd); 252 | richTextBox.CaretPosition = elementToAdd.ElementEnd; 253 | return; 254 | } 255 | 256 | //Insert at the end 257 | paragraph.Inlines.InsertAfter(paragraph.Inlines.LastInline, elementToAdd); 258 | richTextBox.CaretPosition = richTextBox.CaretPosition.DocumentEnd; 259 | } 260 | catch { } 261 | } 262 | public static void ClearParagraph(this RichTextBox richTextBox) 263 | { 264 | richTextBox?.GetParagraph()?.Inlines?.Clear(); 265 | richTextBox?.SetParagraphAsFirstBlock(); 266 | } 267 | 268 | public static bool DragDropAdjustSelection(this RichTextBox richTextBox, Point position) 269 | { 270 | TextPointer textPointer = richTextBox.GetPositionFromPoint(position, true); 271 | int EndOffset = new TextRange(textPointer, richTextBox.Selection.End).Text.Length; 272 | int StartOffset = new TextRange(textPointer, richTextBox.Selection.Start).Text.Length; 273 | if ((EndOffset == 0 || StartOffset == 0) && richTextBox.Selection.Text.Length > 0) 274 | { 275 | //if its on the same richTextBox and the drag and drop position is the same as actual, then we do not perform OnDragDrop. 276 | return false; 277 | } 278 | 279 | //Removal of the drag and drop element to be able to move it 280 | richTextBox.Selection.Text = ""; 281 | richTextBox.CaretPosition = textPointer; 282 | richTextBox.Focus(); 283 | return true; 284 | } 285 | public static DataObject GetDragDropObject(this RichTextBox richTextBox) 286 | { 287 | var objectToSend = richTextBox.GetSelectedObjects(); 288 | if ((objectToSend?.Length ?? 0) == 0) 289 | return null; 290 | 291 | DataObject data = new DataObject(); 292 | data.SetData("Object", objectToSend); 293 | data.SetText(richTextBox.GetSelectedText()); 294 | return data; 295 | } 296 | public static void SetSelectedTextToClipBoard(this RichTextBox richTextBox) 297 | { 298 | try 299 | { 300 | Clipboard.SetText(richTextBox.GetSelectedText()); 301 | } 302 | catch (System.Runtime.InteropServices.COMException ex) 303 | { 304 | //Failed to open Clipboard, this occur when user is copying fast multiples times data 305 | const uint CLIPBRD_E_CANT_OPEN = 0x800401D0; 306 | if ((uint)ex.ErrorCode != CLIPBRD_E_CANT_OPEN) throw; 307 | } 308 | } 309 | 310 | public static Run GetCurrentRunBlock(this RichTextBox richTextBox) => richTextBox?.CaretPosition?.Parent as Run; 311 | public static Paragraph GetParagraph(this RichTextBox richTextBox) => richTextBox?.Document?.Blocks?.FirstBlock as Paragraph; 312 | public static object GetNextItemTag(this RichTextBox richTextBox) 313 | => ((richTextBox.CaretPosition.GetAdjacentElement(LogicalDirection.Forward) as InlineUIContainer)?.Child as FrameworkElement)?.Tag; 314 | public static string GetSelectedText(this RichTextBox richTextBox) 315 | { 316 | string SelectedText = string.Join(string.Empty, 317 | richTextBox.GetParagraph().Inlines 318 | .Where(inline => (inline.ContentStart.CompareTo(richTextBox.Selection.Start) >= 0 && inline.ContentEnd.CompareTo(richTextBox.Selection.End) <= 0)) 319 | .Select(inline => inline.GetText())); 320 | 321 | //Dont forget to add CurrentText 322 | SelectedText += richTextBox.Selection.Text.Trim(); 323 | return SelectedText; 324 | } 325 | 326 | public static object[] GetSelectedObjects(this RichTextBox richTextBox) 327 | => richTextBox?.GetParagraph()?.Inlines 328 | .Where(inline => (inline.ContentStart.CompareTo(richTextBox.Selection.Start) >= 0 && inline.ContentEnd.CompareTo(richTextBox.Selection.End) <= 0)) 329 | .Select(inline => inline.GetObject()) 330 | .Where(i => i != null).ToArray(); 331 | 332 | public static TextBlock GetTextBlock(this Inline inline) 333 | => ((inline as InlineUIContainer)?.Child as TextBlock); 334 | public static object GetObject(this Inline inline) 335 | => GetTextBlock(inline)?.Tag; 336 | public static string GetText(this Inline inline) 337 | => GetTextBlock(inline)?.Text ?? string.Empty; 338 | 339 | #endregion 340 | 341 | #region Popup 342 | public static void Show(this Popup popupElement, Func precondition, Action postAction) => ShowHide(popupElement, precondition, postAction, true); 343 | public static void Hide(this Popup popupElement, Func precondition, Action postAction) => ShowHide(popupElement, precondition, postAction, false); 344 | 345 | private static void ShowHide(this Popup popupElement, Func precondition, Action postAction, bool valueToSet) 346 | { 347 | try 348 | { 349 | if (popupElement == null 350 | || popupElement.IsOpen == valueToSet) 351 | { 352 | return; 353 | } 354 | 355 | bool proceed = precondition == null || precondition(); 356 | if (!proceed) 357 | { 358 | return; 359 | } 360 | 361 | popupElement.IsOpen = valueToSet; 362 | postAction?.Invoke(); 363 | } 364 | catch { } 365 | } 366 | 367 | #endregion 368 | 369 | #region Lookup Contract 370 | public static bool HasAnyExactMatch(this IEnumerable source, string itemString, ILookUpContract contract, object sender) 371 | => source?.Any(i => contract?.IsItemEqualToString(sender, i, itemString) == true) == true; 372 | 373 | public static object GetExactMatch(this IEnumerable source, string itemString, ILookUpContract contract, object sender) 374 | => source?.FirstOrDefault(i => contract?.IsItemEqualToString(sender, i, itemString) == true); 375 | 376 | public static IEnumerable GetSuggestions(this IEnumerable source, string itemString, ILookUpContract contract, object sender) 377 | => source?.Where(i => contract?.IsItemMatchingSearchString(sender, i, itemString) == true); 378 | #endregion 379 | } 380 | } -------------------------------------------------------------------------------- /Source/CoreLibrary/Controls/MultiSelectCombobox/Implementation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Windows; 6 | using System.Windows.Controls; 7 | using System.Windows.Documents; 8 | using System.Windows.Input; 9 | 10 | using BlackPearl.Controls.Extension; 11 | 12 | using EM = BlackPearl.Controls.CoreLibrary.EntensionMethods; 13 | 14 | namespace BlackPearl.Controls.CoreLibrary 15 | { 16 | public sealed partial class MultiSelectCombobox 17 | { 18 | #region Members 19 | private bool isHandlerRegistered = true; 20 | private readonly object handlerLock = new object(); 21 | private const string ObjectString = "Object"; 22 | 23 | #endregion 24 | 25 | #region Control Event Handlers 26 | 27 | private void OnDragEnter(object sender, DragEventArgs e) 28 | => DragDropGetData(e); 29 | 30 | private object DragDropGetData(DragEventArgs e) 31 | { 32 | if (e.Data.GetDataPresent(ObjectString)) 33 | { 34 | var data = e.Data.GetData(ObjectString); 35 | if (data == null) 36 | { 37 | return null; 38 | } 39 | e.Effects = e.KeyStates.HasFlag(DragDropKeyStates.ControlKey) 40 | ? DragDropEffects.Copy 41 | : DragDropEffects.Move; 42 | return data; 43 | } 44 | 45 | if (e.Data.GetDataPresent(DataFormats.Text)) 46 | { 47 | var data = e.Data.GetData(DataFormats.Text); 48 | if (string.IsNullOrWhiteSpace(data.ToString())) 49 | { 50 | return null; 51 | } 52 | e.Effects = DragDropEffects.Copy; 53 | return data; 54 | } 55 | 56 | return null; 57 | } 58 | private void OnDragDrop(object sender, DragEventArgs e) 59 | { 60 | try 61 | { 62 | if (!RichTextBoxElement.DragDropAdjustSelection(e.GetPosition(this))) 63 | { 64 | //if we get here, that mean that the drag and drop final position is the same as it is now. We cancel OnDragDrop. 65 | return; 66 | } 67 | 68 | object data = DragDropGetData(e); 69 | if (data == null) 70 | { 71 | return; 72 | } 73 | 74 | if (data.GetType() == typeof(string)) 75 | { 76 | PasteHandler(data.ToString()); 77 | return; 78 | } 79 | 80 | if (data.GetType() == typeof(object[])) 81 | { 82 | if (!UnsubscribeHandler()) 83 | { 84 | e.Effects = DragDropEffects.None; 85 | return; 86 | } 87 | 88 | foreach (var obj in data as object[]) 89 | { 90 | AddToSelectedItems(obj); 91 | } 92 | } 93 | } 94 | catch { } 95 | finally 96 | { 97 | //Subscribe back 98 | SubsribeHandler(); 99 | } 100 | } 101 | private void OnSelectionStartDrag(object sender, DataObjectCopyingEventArgs e) 102 | { 103 | if (!e.IsDragDrop) 104 | { 105 | return; 106 | } 107 | 108 | var dragDropData = richTextBoxElement.GetDragDropObject(); 109 | if (dragDropData == null) 110 | return; 111 | 112 | var dropResult = DragDrop.DoDragDrop(richTextBoxElement, dragDropData, DragDropEffects.Move | DragDropEffects.Copy); 113 | if (dropResult == DragDropEffects.Move) 114 | { 115 | //If the original RichTextbox is not the same as the one where the drag and drop was performed, then we delete the old text 116 | RichTextBoxElement.Selection.Text = ""; 117 | } 118 | } 119 | private void PasteHandler(object sender, DataObjectPastingEventArgs e) 120 | { 121 | try 122 | { 123 | string clipboard = GetClipboardTextWithCommandCancelled(e); 124 | PasteHandler(clipboard); 125 | } 126 | catch { } 127 | } 128 | 129 | /// 130 | /// Suggestion drop down - key board key up 131 | /// Forces control to update selected item based on selection in suggestion drop down 132 | /// 133 | /// 134 | /// 135 | private void SuggestionElement_PreviewKeyUp(object sender, KeyEventArgs e) => UpdateSelectedItemIfSelectionIsDone(e.Key); 136 | private void SuggestionDropdown_PreviewMouseDown(object sender, MouseButtonEventArgs e) => SuggestionElement.ClearSelection(IsSelectionProcessCompleted); 137 | /// 138 | /// Suggestion drop down - mouse key up 139 | /// Forces control to update selected item based on selection in suggestion drop down 140 | /// 141 | /// 142 | /// 143 | private void SuggestionDropdown_PreviewMouseUp(object sender, MouseButtonEventArgs e) => UpdateSelectedItemIfSelectionIsDone(); 144 | private void MultiSelectCombobox_LostFocus(object sender, RoutedEventArgs e) 145 | { 146 | try 147 | { 148 | //If DropDown has focus, return 149 | if (PopupElement.IsKeyboardFocusWithin) 150 | { 151 | return; 152 | } 153 | 154 | //Remove all invalid texts from 155 | RemoveInvalidTexts(); 156 | 157 | //Deselect text : to fix if we select all, we leave and come back and drag and drop a item 158 | RichTextBoxElement.Selection.Select(RichTextBoxElement.CaretPosition, RichTextBoxElement.CaretPosition); 159 | 160 | //Hide drop-down 161 | HideSuggestions(EM.SuggestionCleanupOperation.ResetIndex | EM.SuggestionCleanupOperation.ClearSelection); 162 | } 163 | catch { } 164 | } 165 | private void MultiSelectCombobox_PreviewKeyDown(object sender, KeyEventArgs e) 166 | { 167 | try 168 | { 169 | //User can remove paragraph reference by 'Select all & delete' in RichTextBox 170 | //Following method call with make sure local paragraph remains part of RichTextBox 171 | RichTextBoxElement.SetParagraphAsFirstBlock(); 172 | 173 | switch (e.Key) 174 | { 175 | case Key.Down: 176 | { 177 | e.Handled = true; 178 | HandleKeyboardDownKeyPress(); 179 | } 180 | break; 181 | case Key.Up: 182 | { 183 | e.Handled = true; 184 | HandleKeyboardUpKeyPress(); 185 | } 186 | break; 187 | case Key.Enter: 188 | { 189 | e.Handled = true; 190 | UpdateSelectedItemsFromSuggestionDropdown(); 191 | } 192 | break; 193 | case Key.Escape: 194 | { 195 | e.Handled = true; 196 | HideSuggestions(EM.SuggestionCleanupOperation.ResetIndex | EM.SuggestionCleanupOperation.ClearSelection); 197 | RichTextBoxElement.TryFocus(); 198 | } 199 | break; 200 | default: 201 | break; 202 | } 203 | } 204 | catch { } 205 | } 206 | private void RichTextBoxElement_TextChanged(object sender, TextChangedEventArgs e) 207 | { 208 | try 209 | { 210 | //All text entered in Control goes to Run element of RichTextBox 211 | string userEnteredText = RichTextBoxElement.GetCurrentText(); 212 | if (!IsEndOfTextDetected(userEnteredText)) 213 | { 214 | UpdateSuggestionAndShowHideDropDown(userEnteredText); 215 | return; 216 | } 217 | 218 | if (!UnsubscribeHandler()) 219 | { 220 | return; 221 | } 222 | //Hide suggestion drop-down 223 | //Reset suggestion drop down list 224 | HideSuggestions(EM.SuggestionCleanupOperation.ResetIndex | EM.SuggestionCleanupOperation.ResetItemSource); 225 | //User is expecting to complete item selection 226 | if (IsBlankTextWithItemSeparator(userEnteredText)) 227 | { 228 | //there's nothing to select 229 | //set current text to empty 230 | RichTextBoxElement.ResetCurrentText(); 231 | return; 232 | } 233 | //User has entered valid text + separator 234 | RichTextBoxElement.RemoveRunBlocks(); 235 | //Try select item from source based on current entered text 236 | UpdateSelectedItemsFromEnteredText(userEnteredText); 237 | } 238 | catch { } 239 | finally 240 | { 241 | //Subscribe back 242 | SubsribeHandler(); 243 | } 244 | } 245 | private void RichTextBoxElement_SizeChanged(object sender, SizeChangedEventArgs e) 246 | { 247 | if (!PopupElement.IsOpen) 248 | { 249 | return; 250 | } 251 | 252 | var offset = PopupElement.HorizontalOffset; 253 | PopupElement.HorizontalOffset = offset + 1; 254 | PopupElement.HorizontalOffset = offset; 255 | } 256 | #endregion 257 | 258 | #region Methods 259 | 260 | #region event handler helper methods 261 | private bool IsBlankTextWithItemSeparator(string userEnteredText) => string.IsNullOrWhiteSpace(userEnteredText.Trim(GetSeparators())); 262 | private bool IsEndOfTextDetected(string userEnteredText) 263 | => !string.IsNullOrEmpty(userEnteredText) && GetSeparators().Any(s => userEnteredText.EndsWith(s.ToString())); 264 | private void UpdateSuggestionAndShowHideDropDown(string userEnteredText) 265 | { 266 | bool hasAnySuggestionToShow = UpdateSuggestions(userEnteredText); 267 | if (hasAnySuggestionToShow) 268 | { 269 | ShowSuggestions(); 270 | return; 271 | } 272 | 273 | HideSuggestions(EM.SuggestionCleanupOperation.ResetIndex | EM.SuggestionCleanupOperation.ClearSelection); 274 | return; 275 | } 276 | private void UpdateSelectedItemIfSelectionIsDone(Key? key = null) 277 | { 278 | if (IsSelectionProcessInProgress(key)) 279 | { 280 | return; 281 | } 282 | 283 | UpdateSelectedItemsFromSuggestionDropdown(); 284 | } 285 | private bool IsSelectionProcessCompleted() => !IsSelectionProcessInProgress(); 286 | private static bool IsSelectionProcessInProgress(Key? keyUp = null) 287 | { 288 | if (!keyUp.HasValue) 289 | { 290 | return Keyboard.Modifiers == ModifierKeys.Control 291 | || Keyboard.Modifiers == ModifierKeys.Shift; 292 | } 293 | 294 | return keyUp != Key.LeftCtrl && keyUp != Key.RightCtrl; 295 | } 296 | /// 297 | /// Removes all invalid texts from RichTextBox except selected item 298 | /// 299 | private void RemoveInvalidTexts() 300 | { 301 | try 302 | { 303 | //Unsubscribe handlers first 304 | if (!UnsubscribeHandler()) 305 | { 306 | //Failed to unsubscribe, return 307 | return; 308 | } 309 | 310 | RichTextBoxElement.RemoveRunBlocks(); 311 | } 312 | finally 313 | { 314 | //Subscribe back 315 | SubsribeHandler(); 316 | } 317 | } 318 | private void PasteHandler(string values) 319 | { 320 | try 321 | { 322 | if (string.IsNullOrWhiteSpace(values)) 323 | { 324 | return; 325 | } 326 | 327 | if (!UnsubscribeHandler()) 328 | { 329 | return; 330 | } 331 | RemoveSelectedItems(); 332 | //User can remove paragraph reference by 'Select all & delete' in RichTextBox 333 | //Following method call with make sure local paragraph remains part of RichTextBox 334 | RichTextBoxElement.SetParagraphAsFirstBlock(); 335 | 336 | //Single item paste 337 | if (values.IndexOfAny(GetSeparators()) == -1) 338 | { 339 | richTextBoxElement.AddToParagraph(values, CreateRunElement); 340 | return; 341 | } 342 | //User has entered valid text + separator 343 | RichTextBoxElement.RemoveRunBlocks(); 344 | 345 | int i; 346 | string[] multipleTexts = values.Split(GetSeparators()); 347 | for (i = 0; i < multipleTexts.Length - 1; i++) 348 | { 349 | //Try select item from source based on current entered text 350 | UpdateSelectedItemsFromEnteredText(multipleTexts[i]); 351 | } 352 | 353 | if (!string.IsNullOrWhiteSpace(multipleTexts[i])) 354 | { 355 | richTextBoxElement.AddToParagraph(multipleTexts[i], CreateRunElement); 356 | } 357 | } 358 | catch { } 359 | finally 360 | { 361 | //Subscribe back 362 | SubsribeHandler(); 363 | } 364 | } 365 | private void RemoveSelectedItems() 366 | { 367 | object[] selectedItems = RichTextBoxElement.GetSelectedObjects(); 368 | 369 | if (selectedItems == null) return; 370 | 371 | foreach (var i in selectedItems) 372 | { 373 | SelectedItems.Remove(i); 374 | } 375 | //if the user paste and has a item inside the selection, then we need to replace it with the pasted content. 376 | //For that, before removing elements, we need to say that we dont want to remove back the tag if the item is unloaded. 377 | //For exemple, if we have a item call "Andréa Müller;" and we select it, and we paste back "Andréa Müller;", 378 | //if Tb_Unloaded is call, "Andréa Müller;" is removed from SelectedItems => then Combobox and SelectedItems is not sync 379 | foreach (var inline in richTextBoxElement.GetParagraph().Inlines) 380 | { 381 | var textblock = inline.GetTextBlock(); 382 | if (textblock != null && selectedItems.Contains(inline.GetObject())) 383 | { 384 | textblock.Unloaded -= Tb_Unloaded; 385 | } 386 | } 387 | //using richTextBoxElement.Selection.Text to empty has the advantage to keep the cursor position 388 | richTextBoxElement.Selection.Text = ""; 389 | } 390 | 391 | 392 | private static string GetClipboardTextWithCommandCancelled(DataObjectPastingEventArgs e) 393 | { 394 | string clipboard = e?.DataObject?.GetData(typeof(string)) as string; 395 | clipboard = clipboard?.Replace("\r", "") 396 | ?.Replace("\t", "") 397 | ?.Replace("\n", ""); 398 | e.CancelCommand(); 399 | e.Handled = true; 400 | return clipboard; 401 | } 402 | private void SetClipboardTextWithCommandCancelled(object sender, ExecutedRoutedEventArgs e) 403 | { 404 | if (e.Command != ApplicationCommands.Copy 405 | && e.Command != ApplicationCommands.Cut) 406 | return; 407 | 408 | e.Handled = true; 409 | RichTextBoxElement.SetSelectedTextToClipBoard(); 410 | 411 | if (e.Command == ApplicationCommands.Cut) 412 | { 413 | //Cut the text = set selection to empty (CTRL + X) 414 | RichTextBoxElement.Selection.Text = ""; 415 | } 416 | } 417 | #endregion 418 | 419 | #region Handler subscribe/unsubscribe 420 | /// 421 | /// Subscribes to events for controls 422 | /// 423 | private void SubsribeHandler() 424 | { 425 | //Check handler registration 426 | if (isHandlerRegistered) 427 | { 428 | //if already registered, return 429 | return; 430 | } 431 | 432 | //acquire a lock 433 | lock (handlerLock) 434 | { 435 | //double check registration 436 | if (isHandlerRegistered) 437 | { 438 | //race condition, return 439 | return; 440 | } 441 | 442 | //set handler flag to true 443 | isHandlerRegistered = true; 444 | 445 | //subscribe 446 | RichTextBoxElement.TextChanged += RichTextBoxElement_TextChanged; 447 | } 448 | } 449 | /// 450 | /// Unsubscribes to events for controls 451 | /// 452 | private bool UnsubscribeHandler() 453 | { 454 | if (RichTextBoxElement == null) 455 | { 456 | return true; 457 | } 458 | 459 | //Check handler registration 460 | if (!isHandlerRegistered) 461 | { 462 | //If already unsubscribed, return 463 | return false; 464 | } 465 | 466 | //acquire a lock 467 | lock (handlerLock) 468 | { 469 | //double check registration 470 | if (!isHandlerRegistered) 471 | { 472 | //race condition, return 473 | return false; 474 | } 475 | 476 | //set handler registration flag 477 | isHandlerRegistered = false; 478 | 479 | //unsubscribe 480 | RichTextBoxElement.TextChanged -= RichTextBoxElement_TextChanged; 481 | 482 | return true; 483 | } 484 | } 485 | #endregion 486 | 487 | #region Selection and Index 488 | private void HandleKeyboardUpKeyPress() 489 | { 490 | if (!HasAnySuggestion()) 491 | { 492 | return; 493 | } 494 | 495 | ShowSuggestions(); 496 | 497 | //If multi-selection 498 | if (Keyboard.Modifiers == ModifierKeys.Shift) 499 | { 500 | SuggestionElement.SelectMultiplePreviousItem(); 501 | return; 502 | } 503 | 504 | SuggestionElement.SelectPreviousItem(); 505 | } 506 | private void HandleKeyboardDownKeyPress() 507 | { 508 | if (!HasAnySuggestion()) 509 | { 510 | return; 511 | } 512 | 513 | ShowSuggestions(); 514 | 515 | //If multi-selection 516 | if (Keyboard.Modifiers == ModifierKeys.Shift) 517 | { 518 | SuggestionElement.SelectMultipleNextItem(); 519 | return; 520 | } 521 | 522 | //Increment selected item index in drop-down 523 | SuggestionElement.SelectNextItem(); 524 | } 525 | /// 526 | /// Tries to set item from entered text in RichTextBox 527 | /// 528 | /// entered text 529 | /// Allows creation of new item 530 | private void UpdateSelectedItemsFromEnteredText(string itemString) 531 | { 532 | if (string.IsNullOrWhiteSpace(itemString)) 533 | { 534 | return; 535 | } 536 | 537 | itemString = itemString.Trim(GetSeparators()).Trim(' '); 538 | 539 | if (IsItemAlreadySelected(itemString)) 540 | { 541 | return; 542 | } 543 | 544 | object itemToAdd = GetItemToAdd(itemString); 545 | //If item is not available 546 | if (itemToAdd == null) 547 | { 548 | return; 549 | } 550 | 551 | AddToSelectedItems(itemToAdd); 552 | RaiseSelectionChangedEvent(new ArrayList(0), new[] { itemToAdd }); 553 | } 554 | private bool IsItemAlreadySelected(string itemString) => SelectedItems?.Cast()?.HasAnyExactMatch(itemString, LookUpContract, this) == true; 555 | private object GetItemToAdd(string itemString) 556 | { 557 | IEnumerable controlItemSource = ItemSource?.Cast(); 558 | 559 | bool hasAnyMatch = controlItemSource.HasAnyExactMatch(itemString, LookUpContract, this); 560 | object itemToAdd = hasAnyMatch //Check if any match 561 | ? controlItemSource.GetExactMatch(itemString, LookUpContract, this) //Exact match is found 562 | : (LookUpContract?.SupportsNewObjectCreation == true) //Check if new object creation is supported by LookUpContract 563 | ? LookUpContract.CreateObject(this, itemString) //Create new object using LookUpContract 564 | : null; //cant create new item. return 565 | return itemToAdd; 566 | } 567 | 568 | private void AddToSelectedItems(object itemToAdd) 569 | { 570 | if (SelectedItems?.Contains(itemToAdd) == true) 571 | { 572 | return; 573 | } 574 | 575 | //Add item in RichTextBox UI 576 | RichTextBoxElement.AddToParagraph(itemToAdd, CreateInlineUIElement); 577 | 578 | var nextItemTag = richTextBoxElement.GetNextItemTag(); 579 | 580 | if (nextItemTag == null || SelectedItems?.Contains(nextItemTag) != true) 581 | { 582 | SelectedItems?.Add(itemToAdd); 583 | return; 584 | } 585 | 586 | SelectedItems?.Insert(SelectedItems.IndexOf(nextItemTag), itemToAdd); 587 | } 588 | 589 | private void AddSuggestionsToSelectedItems(IList itemsToAdd) 590 | { 591 | foreach (object item in itemsToAdd) 592 | { 593 | AddToSelectedItems(item); 594 | } 595 | 596 | RaiseSelectionChangedEvent(new ArrayList(0), new ArrayList(itemsToAdd)); 597 | } 598 | /// 599 | /// Tries to set item from suggestion drop-down 600 | /// 601 | /// 602 | /// 603 | private void UpdateSelectedItemsFromSuggestionDropdown() 604 | { 605 | try 606 | { 607 | //Unsubscribe handlers first 608 | if (!UnsubscribeHandler()) 609 | { 610 | //Failed to unsubscribe, return 611 | return; 612 | } 613 | 614 | //Check if drop down is open or has any item selected 615 | if (!PopupElement.IsOpen || SuggestionElement.SelectedItems.Count < 1) 616 | { 617 | return; 618 | } 619 | 620 | //Remove any user entered text if any 621 | RichTextBoxElement.RemoveRunBlocks(); 622 | 623 | AddSuggestionsToSelectedItems(SuggestionElement.SelectedItems); 624 | 625 | //Hide drop-down 626 | HideSuggestions(EM.SuggestionCleanupOperation.ResetIndex | EM.SuggestionCleanupOperation.ClearSelection | EM.SuggestionCleanupOperation.ResetItemSource); 627 | } 628 | finally 629 | { 630 | //Subscribe back 631 | SubsribeHandler(); 632 | } 633 | 634 | RichTextBoxElement.TryFocus(); 635 | } 636 | #endregion 637 | 638 | #region Suggestion related methods 639 | /// 640 | /// Shows suggestion drop-down 641 | /// 642 | private void ShowSuggestions() => PopupElement.Show(HasAnySuggestion, () => SuggestionElement.CleanOperation(EM.SuggestionCleanupOperation.ResetIndex, ItemSource)); 643 | private void HideSuggestions(EM.SuggestionCleanupOperation cleanupOperation) => PopupElement.Hide(null, () => SuggestionElement.CleanOperation(cleanupOperation, ItemSource)); 644 | private bool HasAnySuggestion() => SuggestionElement.Items.Count > 0; 645 | private bool UpdateSuggestions(string userEnteredText) 646 | { 647 | //Get Items to be shown in suggestion drop-down for current text 648 | IEnumerable itemsToAdd = ItemSource?.Cast().GetSuggestions(userEnteredText, LookUpContract, this); 649 | 650 | //Add suggestion items to suggestion drop-down 651 | SuggestionElement.ItemsSource = itemsToAdd; 652 | 653 | return itemsToAdd?.Any() == true; 654 | } 655 | #endregion 656 | 657 | #region UI Element creation 658 | /// 659 | /// Create RichTextBox document element for given object 660 | /// 661 | /// 662 | /// 663 | private InlineUIContainer CreateInlineUIElement(object objectToDisplay) 664 | { 665 | var tb = new TextBlock() 666 | { 667 | //Text based on Display member path 668 | Text = objectToDisplay.GetPropertyValue(DisplayMemberPath)?.ToString() + ItemSeparator, 669 | //Set object in Tag for easy access for future operations 670 | Tag = objectToDisplay 671 | }; 672 | 673 | tb.Unloaded += Tb_Unloaded; 674 | return new InlineUIContainer(tb); 675 | } 676 | 677 | /// 678 | /// Create RichTextBox document element for given object 679 | /// 680 | /// 681 | /// 682 | private Inline CreateRunElement(object text) 683 | { 684 | Run runElement = richTextBoxElement.GetCurrentRunBlock(); 685 | 686 | if (runElement == null) 687 | { 688 | runElement = richTextBoxElement.GetParagraph().Inlines.LastOrDefault(i => i is Run) as Run; 689 | } 690 | 691 | if (runElement != null) 692 | { 693 | runElement.Text = text.ToString(); 694 | return runElement; 695 | } 696 | 697 | var re = new Run() 698 | { 699 | Text = text?.ToString(), 700 | Language = System.Windows.Markup.XmlLanguage.GetLanguage(System.Globalization.CultureInfo.CurrentCulture.IetfLanguageTag) 701 | }; 702 | 703 | return re; 704 | } 705 | 706 | /// 707 | /// Event to handle scenario where User removes selected item from UI 708 | /// 709 | /// 710 | /// 711 | private void Tb_Unloaded(object sender, RoutedEventArgs e) 712 | { 713 | try 714 | { 715 | if (!IsLoaded || !(sender is TextBlock tb)) 716 | { 717 | return; 718 | } 719 | 720 | tb.Unloaded -= Tb_Unloaded; 721 | SelectedItems?.Remove(tb.Tag); 722 | RaiseSelectionChangedEvent(new[] { tb.Tag }, new ArrayList(0)); 723 | } 724 | catch { } 725 | } 726 | #endregion 727 | 728 | #region Mist 729 | 730 | public char[] GetSeparators() 731 | { 732 | char[] array = new char[1] { ItemSeparator }; 733 | if (AdditionalItemSeparators == null) 734 | { 735 | return array; 736 | } 737 | //Array.Append not available in net 461 738 | return array.Concat(AdditionalItemSeparators).ToArray(); 739 | } 740 | 741 | #endregion 742 | 743 | #endregion 744 | } 745 | } -------------------------------------------------------------------------------- /Source/CoreLibrary/LookUpContracts/DefaultLookUpContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using BlackPearl.Controls.Contract; 4 | using BlackPearl.Controls.Extension; 5 | 6 | namespace BlackPearl.Controls.CoreLibrary 7 | { 8 | public class DefaultLookUpContract : ILookUpContract 9 | { 10 | public bool SupportsNewObjectCreation => false; 11 | 12 | public object CreateObject(object sender, string searchString) => throw new NotSupportedException(); 13 | 14 | public bool IsItemEqualToString(object sender, object item, string seachString) 15 | { 16 | string itemString = item?.GetPropertyValue((sender as MultiSelectCombobox)?.DisplayMemberPath) 17 | ?.ToString(); 18 | return StringEqualsPredicate(itemString, seachString); 19 | } 20 | 21 | public bool IsItemMatchingSearchString(object sender, object item, string searchString) 22 | { 23 | if (string.IsNullOrEmpty(searchString)) 24 | { 25 | return true; 26 | } 27 | 28 | string itemString = item?.GetPropertyValue((sender as MultiSelectCombobox)?.DisplayMemberPath) 29 | ?.ToString(); 30 | return StringStartsWithPredicate(itemString, searchString); 31 | } 32 | 33 | private static bool StringStartsWithPredicate(string value, string searchString) 34 | { 35 | return value != null 36 | && searchString != null 37 | && value.StartsWith(searchString, StringComparison.InvariantCultureIgnoreCase); 38 | } 39 | 40 | private static bool StringEqualsPredicate(string value1, string value2) 41 | { 42 | return value1 != null 43 | && value2 != null 44 | && string.Compare(value1, value2, StringComparison.InvariantCultureIgnoreCase) == 0; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Source/CoreLibrary/LookUpContracts/DiacriticLookUpContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using BlackPearl.Controls.Contract; 4 | using BlackPearl.Controls.Extension; 5 | 6 | namespace BlackPearl.Controls.CoreLibrary 7 | { 8 | public class DiacriticLookUpContract : ILookUpContract 9 | { 10 | public bool SupportsNewObjectCreation => false; 11 | 12 | public object CreateObject(object sender, string searchString) => throw new NotSupportedException(); 13 | 14 | public bool IsItemEqualToString(object sender, object item, string seachString) 15 | { 16 | string itemString = item?.GetPropertyValue((sender as MultiSelectCombobox)?.DisplayMemberPath) 17 | ?.ToString(); 18 | return StringEqualsPredicate(itemString?.RemoveDiacritics(), seachString?.RemoveDiacritics()); 19 | } 20 | 21 | public bool IsItemMatchingSearchString(object sender, object item, string searchString) 22 | { 23 | if (string.IsNullOrEmpty(searchString)) 24 | { 25 | return true; 26 | } 27 | 28 | string itemString = item?.GetPropertyValue((sender as MultiSelectCombobox)?.DisplayMemberPath) 29 | ?.ToString(); 30 | return StringStartsWithPredicate(itemString?.RemoveDiacritics(), searchString?.RemoveDiacritics()); 31 | } 32 | 33 | private static bool StringStartsWithPredicate(string value, string searchString) 34 | { 35 | return value != null 36 | && searchString != null 37 | && value.StartsWith(searchString, StringComparison.InvariantCultureIgnoreCase); 38 | } 39 | 40 | private static bool StringEqualsPredicate(string value1, string value2) 41 | { 42 | return value1 != null 43 | && value2 != null 44 | && string.Compare(value1, value2, StringComparison.InvariantCultureIgnoreCase) == 0; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Source/CoreLibrary/MarkupExtension/EnumBindingSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Reflection; 4 | using System.Windows.Markup; 5 | 6 | namespace BlackPearl.Controls.CoreLibrary 7 | { 8 | //Ref: https://github.com/brianlagunas/BindingEnumsInWpf 9 | public class EnumBindingSourceExtension : MarkupExtension 10 | { 11 | private Type _enumType; 12 | public Type EnumType 13 | { 14 | get { return _enumType; } 15 | set 16 | { 17 | if (value == _enumType) 18 | { 19 | return; 20 | } 21 | 22 | if (null != value) 23 | { 24 | Type enumType = Nullable.GetUnderlyingType(value) ?? value; 25 | 26 | if (!enumType.IsEnum) 27 | throw new ArgumentException("Type must be for an Enum."); 28 | } 29 | 30 | _enumType = value; 31 | } 32 | } 33 | 34 | public EnumBindingSourceExtension() { } 35 | 36 | public EnumBindingSourceExtension(Type enumType) 37 | { 38 | EnumType = enumType; 39 | } 40 | 41 | public override object ProvideValue(IServiceProvider serviceProvider) 42 | { 43 | if (null == _enumType) 44 | throw new InvalidOperationException("The EnumType must be specified."); 45 | 46 | Type actualEnumType = Nullable.GetUnderlyingType(_enumType) ?? _enumType; 47 | Array enumValues = Enum.GetValues(actualEnumType); 48 | 49 | if (actualEnumType == _enumType) 50 | return enumValues; 51 | 52 | Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1); 53 | enumValues.CopyTo(tempArray, 1); 54 | return tempArray; 55 | } 56 | } 57 | 58 | public class EnumDescriptionTypeConverter : EnumConverter 59 | { 60 | public EnumDescriptionTypeConverter(Type type) 61 | : base(type) 62 | { 63 | } 64 | 65 | public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) 66 | { 67 | if (destinationType == typeof(string)) 68 | { 69 | if (value != null) 70 | { 71 | FieldInfo fi = value.GetType().GetField(value.ToString()); 72 | if (fi != null) 73 | { 74 | var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); 75 | return ((attributes.Length > 0) && (!String.IsNullOrEmpty(attributes[0].Description))) ? attributes[0].Description : value.ToString(); 76 | } 77 | } 78 | 79 | return string.Empty; 80 | } 81 | 82 | return base.ConvertTo(context, culture, value, destinationType); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Source/CoreLibrary/Themes/Defaults.xaml: -------------------------------------------------------------------------------- 1 |  3 | 4 | 6 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Source/CoreLibrary/Themes/Generic.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Source/CoreLibrary/Themes/MultiSelectComboboxStyle.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 20 | 21 | 30 | 31 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /Source/Demo/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Source/Demo/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Versioning; 2 | using System.Windows; 3 | 4 | using BlackPearl.Mahapps; 5 | 6 | using MahApps.Metro.Controls; 7 | 8 | using Prism.Ioc; 9 | using Prism.Regions; 10 | using Prism.Unity; 11 | 12 | namespace BlackPearl.Controls.Demo 13 | { 14 | /// 15 | /// Interaction logic for App.xaml 16 | /// 17 | #if NET6_0_OR_GREATER 18 | [SupportedOSPlatform("windows")] 19 | #endif 20 | public partial class App : PrismApplication 21 | { 22 | protected override Window CreateShell() 23 | { 24 | var result = Container.Resolve(); 25 | return result; 26 | } 27 | 28 | protected override void RegisterTypes(IContainerRegistry containerRegistry) 29 | { 30 | containerRegistry.RegisterBlackPearlServices() 31 | .Register() 32 | .Register() 33 | .Register() 34 | .Register() 35 | .RegisterForNavigation(); 36 | 37 | containerRegistry.RegisterForNavigation(Constants.MultiSelectComboBoxView); 38 | } 39 | 40 | protected override void ConfigureRegionAdapterMappings(RegionAdapterMappings regionAdapterMappings) 41 | { 42 | base.ConfigureRegionAdapterMappings(regionAdapterMappings); 43 | regionAdapterMappings.RegisterMapping(); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Source/Demo/BlackPearl.Controls.Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(TargetFrameworkVersion) 4 | WinExe 5 | true 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Source/Demo/LookUpContracts/AdvanceLookUpContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using BlackPearl.Controls.Contract; 5 | 6 | namespace BlackPearl.Controls.Demo 7 | { 8 | public class AdvanceLookUpContract : ILookUpContract 9 | { 10 | public bool SupportsNewObjectCreation => true; 11 | 12 | public object CreateObject(object sender, string searchString) 13 | { 14 | if (searchString?.Count(c => c == ',') != 2) 15 | { 16 | return null; 17 | } 18 | 19 | int firstIndex = searchString.IndexOf(','); 20 | int lastIndex = searchString.LastIndexOf(','); 21 | 22 | return new Person() 23 | { 24 | Name = searchString.Substring(0, firstIndex), 25 | Company = searchString.Substring(firstIndex + 1, lastIndex - firstIndex - 1), 26 | Zip = searchString.Length >= lastIndex ? searchString.Substring(lastIndex + 1) : string.Empty 27 | }; 28 | } 29 | 30 | public bool IsItemEqualToString(object sender, object item, string seachString) 31 | { 32 | if (!(item is Person std)) 33 | { 34 | return false; 35 | } 36 | return string.Compare(seachString, std.Name, StringComparison.InvariantCultureIgnoreCase) == 0; 37 | } 38 | 39 | public bool IsItemMatchingSearchString(object sender, object item, string searchString) 40 | { 41 | if (!(item is Person person)) 42 | { 43 | return false; 44 | } 45 | 46 | if (string.IsNullOrEmpty(searchString)) 47 | { 48 | return true; 49 | } 50 | 51 | return person.Name?.ToLower()?.Contains(searchString?.ToLower()) == true 52 | || person.Company.ToLower()?.Contains(searchString?.ToLower()) == true 53 | || person.Zip?.ToLower()?.Contains(searchString?.ToLower()) == true; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Source/Demo/LookUpContracts/SimpleLookUpContract.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using BlackPearl.Controls.Contract; 4 | using BlackPearl.Controls.CoreLibrary; 5 | using BlackPearl.Controls.Extension; 6 | 7 | namespace BlackPearl.Controls.Demo 8 | { 9 | public class SimpleLookUpContract : ILookUpContract 10 | { 11 | public bool SupportsNewObjectCreation => true; 12 | 13 | public object CreateObject(object sender, string searchString) 14 | { 15 | return new Person() 16 | { 17 | Name = searchString 18 | }; 19 | } 20 | 21 | public bool IsItemEqualToString(object sender, object item, string seachString) 22 | { 23 | string itemString = item?.GetPropertyValue((sender as MultiSelectCombobox)?.DisplayMemberPath) 24 | ?.ToString(); 25 | return StringEqualsPredicate(itemString, seachString); 26 | } 27 | 28 | public bool IsItemMatchingSearchString(object sender, object item, string searchString) 29 | { 30 | if (string.IsNullOrEmpty(searchString)) 31 | { 32 | return true; 33 | } 34 | 35 | string itemString = item?.GetPropertyValue((sender as MultiSelectCombobox)?.DisplayMemberPath) 36 | ?.ToString(); 37 | return StringStartsWithPredicate(itemString, searchString); 38 | } 39 | 40 | private static bool StringStartsWithPredicate(string value, string searchString) 41 | { 42 | return value != null 43 | && searchString != null 44 | && value.StartsWith(searchString, StringComparison.InvariantCultureIgnoreCase); 45 | } 46 | private static bool StringEqualsPredicate(string value1, string value2) 47 | { 48 | return value1 != null 49 | && value2 != null 50 | && string.Compare(value1, value2, StringComparison.InvariantCultureIgnoreCase) == 0; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Source/Demo/Resources/Constants.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | using MahApps.Metro.Controls; 4 | using MahApps.Metro.IconPacks; 5 | 6 | namespace BlackPearl.Controls.Demo 7 | { 8 | public static class Constants 9 | { 10 | public static string ThemeView = "BlackPearlThemeView"; 11 | public static string MultiSelectComboBoxView = "MultiSelectComboBoxView"; 12 | public static string ContentRegion = "ContentRegion"; 13 | 14 | public static readonly ObservableCollection DefaultMenuItems = new ObservableCollection() 15 | { 16 | new HamburgerMenuIconItem() 17 | { 18 | Icon = new PackIconBootstrapIcons() { Kind = PackIconBootstrapIconsKind.Tools, Height = 25, Width = 25 }, 19 | Label = "Controls", 20 | CommandParameter = MultiSelectComboBoxView, 21 | ToolTip = "Controls", 22 | }, 23 | new HamburgerMenuIconItem() 24 | { 25 | Icon = new PackIconEvaIcons() { Kind = PackIconEvaIconsKind.ColorPaletteOutline, Height = 25, Width = 25 }, 26 | Label = "Theme", 27 | CommandParameter = ThemeView, 28 | ToolTip = "Theme", 29 | } 30 | }; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Source/Demo/Resources/DefaultStyle.xaml: -------------------------------------------------------------------------------- 1 |  4 | 5 | 6 | 8 | 9 | 11 | 12 | 15 | 16 | 24 | 31 | -------------------------------------------------------------------------------- /Source/Demo/ShellWindow.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 33 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | -------------------------------------------------------------------------------- /Source/Demo/ShellWindowViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Runtime.Versioning; 3 | using System.Threading.Tasks; 4 | 5 | using BlackPearl.PrismUI; 6 | 7 | using MahApps.Metro.Controls; 8 | 9 | using Prism.Commands; 10 | using Prism.Regions; 11 | 12 | namespace BlackPearl.Controls.Demo 13 | { 14 | 15 | #if NET6_0_OR_GREATER 16 | [SupportedOSPlatform("windows")] 17 | #endif 18 | public class ShellWindowViewModel : BlackPearlViewModel 19 | { 20 | private readonly IRegionManager regionManager; 21 | 22 | public ShellWindowViewModel(IRegionManager regionManager) 23 | { 24 | this.regionManager = regionManager; 25 | } 26 | 27 | public ObservableCollection MenuSource { get; set; } = Constants.DefaultMenuItems; 28 | 29 | public override Task OnLoad() 30 | { 31 | foreach (var item in MenuSource) 32 | { 33 | item.Command = new DelegateCommand(n => regionManager.RequestNavigate(Constants.ContentRegion, n?.ToString())); 34 | } 35 | 36 | regionManager.RequestNavigate(Constants.ContentRegion, Constants.MultiSelectComboBoxView); 37 | 38 | return Task.CompletedTask; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Source/Demo/Views/MultiSelectComboBoxDemoView.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 292 | -------------------------------------------------------------------------------- /Source/Demo/Views/MultiSelectComboBoxDemoViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Windows.Controls; 7 | 8 | using BlackPearl.Controls.Contract; 9 | using BlackPearl.Controls.CoreLibrary; 10 | using BlackPearl.PrismUI; 11 | 12 | using Prism.Commands; 13 | 14 | namespace BlackPearl.Controls.Demo 15 | { 16 | public class MultiSelectComboBoxDemoViewModel : BlackPearlViewModel 17 | { 18 | private readonly IDispatcherService dispatcherService; 19 | private char itemSeparator = ';'; 20 | private char[] additionalItemSeparators = new char[0]; 21 | private PersonDisplayPath selectedDisplayPath = PersonDisplayPath.Name; 22 | private ObservableCollection source = new ObservableCollection(); 23 | private ObservableCollection selectedItems = new ObservableCollection(); 24 | private bool isActive = true; 25 | private ILookUpContract lookupContract = new DefaultLookUpContract(); 26 | private bool includeDiacriticItems = false; 27 | 28 | public MultiSelectComboBoxDemoViewModel(IDispatcherService dispatcherService) 29 | { 30 | this.dispatcherService = dispatcherService; 31 | } 32 | 33 | public bool IsActive 34 | { 35 | get => isActive; 36 | set 37 | { 38 | isActive = value; 39 | RaisePropertyChanged(nameof(IsActive)); 40 | } 41 | } 42 | public DelegateCommand ForceRefreshCommand { get; } 43 | public ObservableCollection Source 44 | { 45 | get => source; 46 | set 47 | { 48 | source = value; 49 | RaisePropertyChanged(nameof(Source)); 50 | } 51 | } 52 | public bool IncludeDiacriticItems 53 | { 54 | get => includeDiacriticItems; 55 | set 56 | { 57 | includeDiacriticItems = value; 58 | RaisePropertyChanged(nameof(IncludeDiacriticItems)); 59 | ForceReloadControl(changeInItemSource: true); 60 | } 61 | } 62 | 63 | public ObservableCollection SelectedItems 64 | { 65 | get => selectedItems; 66 | set 67 | { 68 | selectedItems = value; 69 | RaisePropertyChanged(nameof(SelectedItems)); 70 | RaisePropertyChanged(nameof(SelectedItemsString)); 71 | } 72 | } 73 | public string SelectedItemsString => string.Join(ItemSeparator.ToString(), SelectedItems.Select(p => p.Name)); 74 | public DelegateCommand SelectionChangedEventCommand => new DelegateCommand(SelectionChangedEventAction); 75 | 76 | public string DisplayMemberPath => SelectedDisplayPath.ToString(); 77 | public PersonDisplayPath SelectedDisplayPath 78 | { 79 | get => selectedDisplayPath; 80 | set 81 | { 82 | selectedDisplayPath = value; 83 | RaisePropertyChanged(nameof(SelectedDisplayPath)); 84 | RaisePropertyChanged(nameof(DisplayMemberPath)); 85 | ForceReloadControl(); 86 | } 87 | } 88 | 89 | public char[] ItemSeparatorSource => new char[] { ';', ',', '|', '~' }; 90 | public char ItemSeparator 91 | { 92 | get => itemSeparator; 93 | set 94 | { 95 | itemSeparator = value; 96 | RaisePropertyChanged(nameof(ItemSeparator)); 97 | ForceReloadControl(); 98 | } 99 | } 100 | public char[] AdditionalItemSeparators 101 | { 102 | get => additionalItemSeparators; 103 | set 104 | { 105 | additionalItemSeparators = value; 106 | RaisePropertyChanged(nameof(AdditionalItemSeparators)); 107 | ForceReloadControl(); 108 | } 109 | } 110 | public DelegateCommand AdditionalSeparatorCheckCommand => new DelegateCommand(SetAdditionalSeparator); 111 | 112 | public bool IsDefaultContract => LookupContract is DefaultLookUpContract; 113 | public bool IsDiacriticContract => LookupContract is DiacriticLookUpContract; 114 | public bool IsCustomContract => LookupContract is AdvanceLookUpContract; 115 | public ILookUpContract LookupContract 116 | { 117 | get => lookupContract; 118 | set 119 | { 120 | lookupContract = value; 121 | RaisePropertyChanged(nameof(LookupContract)); 122 | RaisePropertyChanged(nameof(IsDefaultContract)); 123 | RaisePropertyChanged(nameof(IsDiacriticContract)); 124 | RaisePropertyChanged(nameof(IsCustomContract)); 125 | } 126 | } 127 | public DelegateCommand ChangeLookupContractCommand => new DelegateCommand(ChangeLookupContract); 128 | 129 | private async void ForceReloadControl(bool changeInItemSource = false) 130 | { 131 | try 132 | { 133 | IsActive = false; 134 | 135 | List selection = null; 136 | if (changeInItemSource) 137 | { 138 | await SetPersonItemSource(); 139 | } 140 | else 141 | { 142 | selection = SelectedItems.ToList(); 143 | } 144 | 145 | await SetPersonSelectedItemRandom(selection); 146 | } 147 | catch { } 148 | finally 149 | { 150 | IsActive = true; 151 | } 152 | } 153 | 154 | public override bool KeepAlive => true; 155 | public override async Task OnLoad() 156 | { 157 | try 158 | { 159 | IsActive = false; 160 | await SetPersonItemSource(); 161 | 162 | await SetPersonSelectedItemRandom(); 163 | } 164 | catch { } 165 | finally 166 | { 167 | IsActive = true; 168 | } 169 | } 170 | private void SelectionChangedEventAction(SelectionChangedEventArgs eventArgs) => RaisePropertyChanged(nameof(SelectedItemsString)); 171 | private void SetAdditionalSeparator(CheckBox checkBox) 172 | { 173 | if (checkBox == null) 174 | return; 175 | 176 | var separator = checkBox.Content.ToString()[0]; 177 | if (checkBox.IsChecked == true) 178 | { 179 | var newValue = AdditionalItemSeparators.ToList(); 180 | newValue.Add(separator); 181 | AdditionalItemSeparators = newValue.ToArray(); 182 | return; 183 | } 184 | 185 | AdditionalItemSeparators = AdditionalItemSeparators.Where(i => i != separator).ToArray(); 186 | } 187 | private async Task SetPersonSelectedItemRandom(System.Collections.Generic.List currentSelection = null) 188 | { 189 | if (currentSelection == null) 190 | //if (currentSelection?.Any() != true) 191 | { 192 | var item1 = Source[new Random().Next(0, Source.Count - 1)]; 193 | currentSelection = new System.Collections.Generic.List { item1 }; 194 | } 195 | 196 | await dispatcherService.Execute(() => 197 | { 198 | var newSelection = new ObservableCollection(currentSelection); 199 | SelectedItems = newSelection; 200 | }); 201 | } 202 | private async Task SetPersonItemSource() 203 | { 204 | var data = await Task.Run(() => PersonDataProvider.GetDummyData()); 205 | 206 | if (!IncludeDiacriticItems) 207 | { 208 | data = data.Skip(20); 209 | } 210 | 211 | await dispatcherService.Execute(() => 212 | { 213 | Source = new ObservableCollection(data); 214 | }); 215 | } 216 | private void ChangeLookupContract(string index) 217 | { 218 | if (index == null || index == "0") 219 | { 220 | LookupContract = new DefaultLookUpContract(); 221 | } 222 | else if (index == "1") 223 | { 224 | LookupContract = new DiacriticLookUpContract(); 225 | } 226 | else 227 | { 228 | LookupContract = new AdvanceLookUpContract(); 229 | } 230 | 231 | ForceReloadControl(); 232 | } 233 | } 234 | 235 | public enum PersonDisplayPath 236 | { 237 | Name, 238 | Company, 239 | City, 240 | Zip, 241 | Info 242 | } 243 | } -------------------------------------------------------------------------------- /Source/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0-windows7.0;net48 4 | 2.0.4.0 5 | 2.0.4.0 6 | 2.0.4.0 7 | true 8 | disable 9 | disable 10 | 11 | -------------------------------------------------------------------------------- /Source/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Source/Mahapps/BlackPearl.Mahapps.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(TargetFrameworkVersion) 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Source/Mahapps/RegionAdapter/HamburgerMenuSingleRegionAdapter.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Versioning; 2 | 3 | using MahApps.Metro.Controls; 4 | 5 | using Prism.Regions; 6 | 7 | namespace BlackPearl.Mahapps 8 | { 9 | #if NET6_0_OR_GREATER 10 | [SupportedOSPlatform("windows")] 11 | #endif 12 | public class HamburgerMenuSingleRegionAdapter : RegionAdapterBase 13 | { 14 | public HamburgerMenuSingleRegionAdapter(IRegionBehaviorFactory factory) : base(factory) { } 15 | protected override void Adapt(IRegion region, HamburgerMenu regionTarget) 16 | { 17 | region.ActiveViews.CollectionChanged += (s, e) => 18 | { 19 | if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add 20 | && (e.NewItems?.Count ?? 0) > 0) 21 | { 22 | regionTarget.Content = e.NewItems[0]; 23 | } 24 | }; 25 | } 26 | 27 | protected override IRegion CreateRegion() => new SingleActiveRegion(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Source/Mahapps/Resources/DefaultStyle.xaml: -------------------------------------------------------------------------------- 1 |  4 | 14 5 | 12 6 | 1 7 | 24 8 | 14 9 | 5 10 | 11 | 27 | 28 | 39 | 40 | 48 | 49 | 61 | 62 | 74 | 75 | 87 | 88 | 100 | 101 | 115 | 116 | 130 | 131 | 145 | 146 | -------------------------------------------------------------------------------- /Source/Mahapps/UI_Base/BlackPearlDialogWindow.cs: -------------------------------------------------------------------------------- 1 | using Prism.Services.Dialogs; 2 | 3 | namespace BlackPearl.Mahapps 4 | { 5 | public class BlackPearlDialogWindow : BlackPearlMetroWindow, IDialogWindow 6 | { 7 | public IDialogResult Result { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Source/Mahapps/UI_Base/BlackPearlMetroWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | using System.Windows.Markup; 5 | 6 | using BlackPearl.PrismUI; 7 | 8 | using MahApps.Metro.Controls; 9 | 10 | namespace BlackPearl.Mahapps 11 | { 12 | public abstract class BlackPearlMetroWindow : MetroWindow 13 | { 14 | public BlackPearlMetroWindow() 15 | { 16 | Loaded += Window_Loaded; 17 | Unloaded += Window_Unloaded; 18 | 19 | (this as IComponentConnector)?.InitializeComponent(); 20 | } 21 | 22 | private async void Window_Unloaded(object sender, RoutedEventArgs e) 23 | { 24 | Unloaded -= Window_Unloaded; 25 | 26 | try 27 | { 28 | await this.DataContextAction(vm => vm.OnUnload()); 29 | await this.DataContextAction(vm => 30 | { 31 | vm.Dispose(); 32 | return Task.CompletedTask; 33 | }); 34 | } 35 | catch { } 36 | } 37 | 38 | private async void Window_Loaded(object sender, RoutedEventArgs e) 39 | { 40 | Loaded -= Window_Loaded; 41 | 42 | try 43 | { 44 | await this.DataContextAction(vm => vm.OnLoad()); 45 | } 46 | catch { } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Source/Mahapps/Utility/BlackPearlThemeManager.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using System.Windows.Media; 3 | 4 | using ControlzEx.Theming; 5 | 6 | namespace BlackPearl.Mahapps 7 | { 8 | public interface IBlackPearlThemeManager 9 | { 10 | void DecreaseFontSize(); 11 | Color GetAccentColor(); 12 | void IncreaseFontSize(); 13 | bool IsDarkTheme(); 14 | void SetTheme(Color accentColor, bool isDark); 15 | } 16 | 17 | public class BlackPearlThemeManager : IBlackPearlThemeManager 18 | { 19 | public const string H1FontSize = "BlackPearl.H1FontSize"; 20 | public const string H2FontSize = "BlackPearl.H2FontSize"; 21 | public const string FontSizeIncrementBy = "BlackPearl.FontSizeIncrementBy"; 22 | public const string MaxH1FontSize = "BlackPearl.MaxH1FontSize"; 23 | public const string MinH1FontSize = "BlackPearl.MinH1FontSize"; 24 | public bool IsDarkTheme() 25 | => ThemeManager.Current.DetectTheme()?.BaseColorScheme == ThemeManager.BaseColorDarkConst; 26 | public Color GetAccentColor() 27 | => (Color)ColorConverter.ConvertFromString(ThemeManager.Current.DetectTheme()?.ColorScheme ?? "Blue"); 28 | public void SetTheme(Color accentColor, bool isDark) 29 | { 30 | try 31 | { 32 | var baseColor = isDark ? ThemeManager.BaseColorDarkConst : ThemeManager.BaseColorLightConst; 33 | var theme = RuntimeThemeGenerator.Current.GenerateRuntimeTheme(baseColor, accentColor); 34 | ThemeManager.Current.ChangeTheme(Application.Current, theme); 35 | } 36 | catch { } 37 | } 38 | public void IncreaseFontSize() 39 | { 40 | var v = Application.Current.Resources.Contains(H1FontSize); 41 | 42 | var currentValue = (double)Application.Current.Resources[H1FontSize]; 43 | var maxH1FontSize = (double)Application.Current.Resources[MaxH1FontSize]; 44 | var incrementBy = (double)Application.Current.Resources[FontSizeIncrementBy]; 45 | 46 | if (currentValue >= maxH1FontSize) 47 | { 48 | return; 49 | } 50 | 51 | var newValue = (currentValue + incrementBy) > maxH1FontSize ? maxH1FontSize : currentValue + incrementBy; 52 | 53 | Application.Current.Resources[H1FontSize] = newValue; 54 | Application.Current.Resources[H2FontSize] = newValue - incrementBy; 55 | } 56 | public void DecreaseFontSize() 57 | { 58 | var currentValue = (double)Application.Current.Resources[H1FontSize]; 59 | var minH1FontSize = (double)Application.Current.Resources[MinH1FontSize]; 60 | var incrementBy = (double)Application.Current.Resources[FontSizeIncrementBy]; 61 | 62 | if (currentValue <= minH1FontSize) 63 | { 64 | return; 65 | } 66 | 67 | var newValue = (currentValue - incrementBy) < minH1FontSize ? minH1FontSize : currentValue - incrementBy; 68 | Application.Current.Resources[H1FontSize] = newValue; 69 | Application.Current.Resources[H2FontSize] = newValue - incrementBy; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Source/Mahapps/Utility/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | using BlackPearl.PrismUI; 4 | 5 | using Prism.Ioc; 6 | 7 | namespace BlackPearl.Mahapps 8 | { 9 | public static class Extensions 10 | { 11 | public static IContainerRegistry RegisterBlackPearlServices(this IContainerRegistry registry) 12 | { 13 | registry.RegisterBlackPearlCoreServices() 14 | .RegisterSingleton() 15 | .Register(); 16 | 17 | registry.RegisterForNavigation("BlackPearlThemeView"); 18 | 19 | Application.Current.Resources.MergedDictionaries.Add(new ResourceDictionary() { Source = new System.Uri("pack://application:,,,/BlackPearl.Mahapps;component/Resources/DefaultStyle.xaml") }); 20 | 21 | return registry; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Source/Mahapps/View/ThemeView.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | 36 | 37 | 38 | 39 | 111 | -------------------------------------------------------------------------------- /Source/Mahapps/View/ThemeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Windows; 6 | using System.Windows.Media; 7 | 8 | using BlackPearl.PrismUI; 9 | 10 | using Prism.Commands; 11 | using Prism.Events; 12 | 13 | 14 | namespace BlackPearl.Mahapps 15 | { 16 | public class ThemeViewModel : BlackPearlViewModel 17 | { 18 | #region Members 19 | private readonly IBlackPearlThemeManager themeManager; 20 | private readonly IDispatcherService dispatcherService; 21 | private readonly IEventAggregator eventAggregator; 22 | private bool isDark; 23 | private AccentInfo selectedAccent; 24 | #endregion 25 | 26 | #region Constructor 27 | public ThemeViewModel(IBlackPearlThemeManager themeManager, IDispatcherService dispatcherService, IEventAggregator eventAggregator) 28 | { 29 | this.themeManager = themeManager; 30 | this.dispatcherService = dispatcherService; 31 | this.eventAggregator = eventAggregator; 32 | } 33 | #endregion 34 | 35 | #region Properties 36 | public ObservableCollection Accents { get; set; } = new ObservableCollection(); 37 | public AccentInfo SelectedAccent 38 | { 39 | get => selectedAccent; 40 | set 41 | { 42 | selectedAccent = value; 43 | AccentChanged(); 44 | } 45 | } 46 | public string ThemeText => IsDark ? "Dark" : "Light"; 47 | public Visibility MoonVisibility => IsDark ? Visibility.Visible : Visibility.Collapsed; 48 | public Visibility SunVisibility => !IsDark ? Visibility.Visible : Visibility.Collapsed; 49 | public DelegateCommand IncreaseFontCommand => new DelegateCommand(IncreaseFont); 50 | public DelegateCommand DecreaseFontCommand => new DelegateCommand(DecreaseFont); 51 | public bool IsDark 52 | { 53 | get => isDark; 54 | set 55 | { 56 | isDark = value; 57 | IsDarkChanged(); 58 | } 59 | } 60 | #endregion 61 | 62 | #region Methods 63 | public override async Task OnLoad() 64 | { 65 | try 66 | { 67 | var allAccentInfo = AccentInfo.GetAll(); 68 | var accentColor = themeManager.GetAccentColor(); 69 | isDark = themeManager.IsDarkTheme(); 70 | var accentInfo = allAccentInfo.FirstOrDefault(a => a.Value == accentColor); 71 | 72 | await dispatcherService.Execute(() => 73 | { 74 | Accents.AddRange(allAccentInfo); 75 | selectedAccent = accentInfo; 76 | 77 | RaisePropertyChanged(nameof(SelectedAccent)); 78 | RaisePropertyChanged(nameof(IsDark)); 79 | RaisePropertyChanged(nameof(ThemeText)); 80 | RaisePropertyChanged(nameof(MoonVisibility)); 81 | RaisePropertyChanged(nameof(SunVisibility)); 82 | }); 83 | } 84 | catch { } 85 | } 86 | private void DecreaseFont() 87 | { 88 | try 89 | { 90 | themeManager.DecreaseFontSize(); 91 | } 92 | catch { } 93 | } 94 | private void IncreaseFont() 95 | { 96 | try 97 | { 98 | themeManager.IncreaseFontSize(); 99 | } 100 | catch { } 101 | } 102 | private void AccentChanged() 103 | { 104 | try 105 | { 106 | themeManager.SetTheme(SelectedAccent.Value, IsDark); 107 | } 108 | catch { } 109 | } 110 | private void IsDarkChanged() 111 | { 112 | try 113 | { 114 | themeManager.SetTheme(SelectedAccent.Value, IsDark); 115 | RaisePropertyChanged(nameof(ThemeText)); 116 | RaisePropertyChanged(nameof(MoonVisibility)); 117 | RaisePropertyChanged(nameof(SunVisibility)); 118 | } 119 | catch { } 120 | } 121 | #endregion 122 | } 123 | 124 | public class AccentInfo 125 | { 126 | public string Name { get; set; } 127 | public Color Value { get; set; } 128 | public Brush Brush => new SolidColorBrush(Value); 129 | public static List GetAll() 130 | => typeof(Colors).GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.GetProperty) 131 | .Select(p => new AccentInfo() 132 | { 133 | Name = p.Name, 134 | Value = (Color)p.GetValue(null) 135 | }).ToList(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Source/MainSolution.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.7.34031.279 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlackPearl.Controls.Common", "Common\BlackPearl.Controls.Common.csproj", "{D6866289-89D6-4A76-A935-443BC30D07AE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlackPearl.Controls.CoreLibrary", "CoreLibrary\BlackPearl.Controls.CoreLibrary.csproj", "{09779FE5-D1BF-4EFB-B259-4471A46B92D4}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlackPearl.Controls.Demo", "Demo\BlackPearl.Controls.Demo.csproj", "{EB730717-05BB-4488-808F-6C18F359D652}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlackPearl.Prism", "Prism\BlackPearl.Prism.csproj", "{ED40637F-3160-4EEA-9C6C-9D8F56983A3D}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlackPearl.Mahapps", "Mahapps\BlackPearl.Mahapps.csproj", "{DD3F3845-051A-41BA-9F0A-8A4ABEABE15A}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EC41479F-5E2C-4DE8-8E67-FA6185F5B061}" 17 | ProjectSection(SolutionItems) = preProject 18 | Directory.Build.props = Directory.Build.props 19 | Directory.Packages.props = Directory.Packages.props 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {D6866289-89D6-4A76-A935-443BC30D07AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {D6866289-89D6-4A76-A935-443BC30D07AE}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {D6866289-89D6-4A76-A935-443BC30D07AE}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {D6866289-89D6-4A76-A935-443BC30D07AE}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {09779FE5-D1BF-4EFB-B259-4471A46B92D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {09779FE5-D1BF-4EFB-B259-4471A46B92D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {09779FE5-D1BF-4EFB-B259-4471A46B92D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {09779FE5-D1BF-4EFB-B259-4471A46B92D4}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {EB730717-05BB-4488-808F-6C18F359D652}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {EB730717-05BB-4488-808F-6C18F359D652}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {EB730717-05BB-4488-808F-6C18F359D652}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {EB730717-05BB-4488-808F-6C18F359D652}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {ED40637F-3160-4EEA-9C6C-9D8F56983A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {ED40637F-3160-4EEA-9C6C-9D8F56983A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {ED40637F-3160-4EEA-9C6C-9D8F56983A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {ED40637F-3160-4EEA-9C6C-9D8F56983A3D}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {DD3F3845-051A-41BA-9F0A-8A4ABEABE15A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {DD3F3845-051A-41BA-9F0A-8A4ABEABE15A}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {DD3F3845-051A-41BA-9F0A-8A4ABEABE15A}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {DD3F3845-051A-41BA-9F0A-8A4ABEABE15A}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {3230BC09-2437-47F9-94B2-B0F6F858E264} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /Source/Prism/BlackPearl.Prism.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(TargetFrameworkVersion) 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Source/Prism/Services/DispatcherService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | 5 | namespace BlackPearl.PrismUI 6 | { 7 | public interface IDispatcherService 8 | { 9 | Task Execute(Action action); 10 | } 11 | 12 | public class DispatcherService : IDispatcherService 13 | { 14 | public async Task Execute(Action action) 15 | { 16 | bool isSuccess = true; 17 | if (Application.Current.Dispatcher.CheckAccess()) 18 | { 19 | try 20 | { 21 | action(); 22 | } 23 | catch 24 | { 25 | isSuccess = false; 26 | } 27 | return isSuccess; 28 | } 29 | 30 | await Application.Current.Dispatcher.InvokeAsync(() => 31 | { 32 | try 33 | { 34 | action(); 35 | } 36 | catch 37 | { 38 | isSuccess = false; 39 | } 40 | }); 41 | 42 | return isSuccess; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Prism/UI_Base/BlackPearlUserControl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows.Controls; 4 | using System.Windows.Markup; 5 | 6 | using Prism.Regions; 7 | 8 | namespace BlackPearl.PrismUI 9 | { 10 | public abstract class BlackPearlUserControl : UserControl 11 | { 12 | public BlackPearlUserControl() 13 | { 14 | Loaded += Control_Loaded; 15 | Unloaded += Control_Unloaded; 16 | 17 | (this as IComponentConnector)?.InitializeComponent(); 18 | } 19 | 20 | private async void Control_Unloaded(object sender, System.Windows.RoutedEventArgs e) 21 | { 22 | Unloaded -= Control_Unloaded; 23 | 24 | try 25 | { 26 | if (DataContext is IRegionMemberLifetime rlm && rlm.KeepAlive) 27 | { 28 | return; 29 | } 30 | 31 | await this.DataContextAction(vm => vm.OnUnload()); 32 | await this.DataContextAction(vm => 33 | { 34 | vm.Dispose(); 35 | return Task.CompletedTask; 36 | }); 37 | } 38 | catch { } 39 | } 40 | 41 | private async void Control_Loaded(object sender, System.Windows.RoutedEventArgs e) 42 | { 43 | Loaded -= Control_Loaded; 44 | 45 | try 46 | { 47 | await this.DataContextAction(vm => vm.OnLoad()); 48 | } 49 | catch { } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Source/Prism/UI_Base/BlackPearlViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | using Prism.Mvvm; 10 | using Prism.Regions; 11 | 12 | namespace BlackPearl.PrismUI 13 | { 14 | public class BlackPearlViewModel : BindableBase, IDisposable, INavigationAware, IRegionMemberLifetime, INotifyDataErrorInfo 15 | { 16 | #region Members 17 | private readonly IEnumerable availableProperties; 18 | private ConcurrentDictionary> propertyErrors = new ConcurrentDictionary>(); 19 | #endregion 20 | 21 | #region Constructor 22 | public BlackPearlViewModel() 23 | { 24 | availableProperties = GetType() 25 | .GetProperties() 26 | .Where(p => p.Name != nameof(HasErrors)) 27 | .Select(p => p.Name); 28 | PropertyChanged += ViewModel_PropertyChanged; 29 | } 30 | #endregion 31 | 32 | #region Methods 33 | public virtual Task OnLoad() => Task.CompletedTask; 34 | public virtual Task OnUnload() => Task.CompletedTask; 35 | protected virtual IEnumerable GetValidationErrors(string propertyName) => null; 36 | 37 | protected void AddValidations(string propertyName, IEnumerable errors) 38 | { 39 | propertyErrors?.AddOrUpdate(propertyName, errors.ToList(), 40 | (key, oldErrors) => errors.Union(oldErrors).Distinct().ToList()); 41 | 42 | ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); 43 | } 44 | protected void Validate(string propertyName) 45 | { 46 | if (disposedValue || string.IsNullOrEmpty(propertyName)) 47 | { 48 | return; 49 | } 50 | 51 | ClearPropertyError(propertyName); 52 | IEnumerable errors = null; 53 | 54 | try 55 | { 56 | errors = GetValidationErrors(propertyName); 57 | } 58 | catch { } 59 | 60 | if (errors?.Any() != true) 61 | { 62 | return; 63 | } 64 | 65 | propertyErrors?.AddOrUpdate(propertyName, errors.ToList(), 66 | (key, oldErrors) => errors.Union(oldErrors).Distinct().ToList()); 67 | 68 | ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); 69 | } 70 | protected void ValidateAll() 71 | { 72 | if (disposedValue) 73 | { 74 | return; 75 | } 76 | 77 | foreach (string p in availableProperties) 78 | { 79 | Validate(p); 80 | } 81 | } 82 | protected void ClearValidation(string propertyName) 83 | { 84 | if (disposedValue) 85 | { 86 | return; 87 | } 88 | 89 | ClearPropertyError(propertyName); 90 | ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName)); 91 | } 92 | 93 | private void ClearPropertyError(string propertyName) 94 | { 95 | if (propertyErrors?.ContainsKey(propertyName) != true) 96 | { 97 | return; 98 | } 99 | 100 | propertyErrors[propertyName]?.Clear(); 101 | } 102 | private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e) => Validate(e?.PropertyName); 103 | #endregion 104 | 105 | public virtual bool KeepAlive => false; 106 | 107 | #region INotifyDataErrorInfo 108 | public bool HasErrors => propertyErrors?.Any(pe => pe.Value?.Any() == true) == true; 109 | public event EventHandler ErrorsChanged; 110 | public IEnumerable GetErrors(string propertyName) 111 | => (!disposedValue && !string.IsNullOrEmpty(propertyName) && propertyErrors?.ContainsKey(propertyName) == true) 112 | ? propertyErrors[propertyName] 113 | : (IEnumerable)new List(0); 114 | #endregion 115 | 116 | #region INavigationAware 117 | 118 | public virtual void OnNavigatedTo(NavigationContext navigationContext) { } 119 | public virtual bool IsNavigationTarget(NavigationContext navigationContext) => !disposedValue; 120 | public virtual void OnNavigatedFrom(NavigationContext navigationContext) { } 121 | #endregion 122 | 123 | #region IDisposable 124 | private bool disposedValue; 125 | protected virtual void Dispose(bool disposing) 126 | { 127 | if (disposedValue) 128 | { 129 | return; 130 | } 131 | 132 | if (disposing) 133 | { 134 | PropertyChanged -= ViewModel_PropertyChanged; 135 | propertyErrors = null; 136 | } 137 | 138 | disposedValue = true; 139 | } 140 | 141 | void IDisposable.Dispose() 142 | { 143 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 144 | Dispose(disposing: true); 145 | GC.SuppressFinalize(this); 146 | } 147 | 148 | #endregion 149 | } 150 | } -------------------------------------------------------------------------------- /Source/Prism/UI_Base/BlackPearlWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | using System.Windows.Markup; 5 | 6 | namespace BlackPearl.PrismUI 7 | { 8 | public abstract class BlackPearlWindow : Window 9 | { 10 | public BlackPearlWindow() 11 | { 12 | Loaded += Window_Loaded; 13 | Unloaded += Window_Unloaded; 14 | 15 | (this as IComponentConnector)?.InitializeComponent(); 16 | } 17 | 18 | private async void Window_Unloaded(object sender, RoutedEventArgs e) 19 | { 20 | Unloaded -= Window_Unloaded; 21 | 22 | try 23 | { 24 | await this.DataContextAction(vm => vm.OnUnload()); 25 | await this.DataContextAction(vm => 26 | { 27 | vm.Dispose(); 28 | return Task.CompletedTask; 29 | }); 30 | } 31 | catch { } 32 | } 33 | 34 | private async void Window_Loaded(object sender, RoutedEventArgs e) 35 | { 36 | Loaded -= Window_Loaded; 37 | 38 | try 39 | { 40 | await this.DataContextAction(vm => vm.OnLoad()); 41 | } 42 | catch { } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Source/Prism/Utility/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using System.Windows; 4 | 5 | using Prism.Events; 6 | using Prism.Ioc; 7 | 8 | namespace BlackPearl.PrismUI 9 | { 10 | public static class Extensions 11 | { 12 | public static IContainerRegistry RegisterBlackPearlCoreServices(this IContainerRegistry registry) 13 | => registry?.Register(); 14 | public static async Task DataContextAction(this FrameworkElement frameworkElement, Func action) 15 | { 16 | if (!(frameworkElement?.DataContext is T vm) || vm == null) 17 | { 18 | return false; 19 | } 20 | 21 | await action(vm); 22 | return true; 23 | } 24 | public static IEventAggregator Subscribe(this IEventAggregator eventAggregator, Action action) 25 | { 26 | eventAggregator.GetEvent>() 27 | .Subscribe(action, ThreadOption.BackgroundThread, keepSubscriberReferenceAlive: false); 28 | return eventAggregator; 29 | } 30 | public static IEventAggregator Publish(this IEventAggregator eventAggregator, TPayload data) 31 | { 32 | eventAggregator.GetEvent>() 33 | .Publish(data); 34 | return eventAggregator; 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker --------------------------------------------------------------------------------