├── .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 | 
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 | 
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 | 
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 | [](https://nilayjoshi89.visualstudio.com/BlackPearl%202.0/_build/latest?definitionId=5&branchName=master)
3 | [](https://www.nuget.org/packages/BlackPearl.Controls.Library/)
4 | [](https://www.nuget.org/packages/BlackPearl.Controls.Library/)
5 | 
6 | [](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 | 
33 |
34 | 
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