├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── .gitignore ├── Images ├── logo.png └── logo_gradient.png ├── ObservableView.sln ├── ObservableView ├── Exceptions │ └── SearchTextPreprocessorException.cs ├── Extensions │ ├── EnumerableExtensions.cs │ ├── ExpressionExtensions.cs │ ├── ObservableViewExtensions.cs │ ├── OperationExtensions.cs │ ├── QueryableExtensions.cs │ ├── ReflectionHelper.cs │ ├── SortDirectionExtensions.cs │ ├── StringExtensions.cs │ └── TypeExtensions.cs ├── Filtering │ ├── FilterEventArgs.cs │ └── FilterEventHandler.cs ├── GlobalUsings.cs ├── Grouping │ ├── AlphaGroupKeyAlgorithm.cs │ ├── GroupKeyAlgorithm.cs │ ├── Grouping.cs │ ├── IGroupKeyAlgorithm.cs │ └── MonthGroupAlgorithm.cs ├── IObservableView.cs ├── ObservableObject.cs ├── ObservableView.cs ├── ObservableView.csproj ├── PreserveAttribute.cs ├── Properties │ └── AssemblyInfo.cs ├── SearchLogic.cs ├── Searching │ ├── ExpressionBuilder.cs │ ├── IExpressionBuilder.cs │ ├── ISearchSpecification.cs │ ├── Operands │ │ ├── ConstantOperand.cs │ │ ├── IOperand.cs │ │ ├── Operand.cs │ │ ├── Operation.cs │ │ ├── PropertyOperand.cs │ │ └── VariableOperand.cs │ ├── Operations │ │ ├── BinaryOperation.cs │ │ └── GroupOperation.cs │ ├── Operators │ │ ├── AndOperator.cs │ │ ├── BinaryOperator.cs │ │ ├── ContainsOperator.cs │ │ ├── EqualOperator.cs │ │ ├── GroupOperator.cs │ │ ├── IOperator.cs │ │ └── OrOperator.cs │ ├── Processors │ │ ├── ExpressionProcessor.cs │ │ ├── IExpressionProcessor.cs │ │ ├── ToLowerExpressionProcessor.cs │ │ ├── ToUpperExpressionProcessor.cs │ │ └── TrimExpressionProcessor.cs │ ├── SearchSpecification.cs │ └── SearchableAttribute.cs ├── Sorting │ ├── NaturalSortComparable.cs │ ├── OrderDirection.cs │ └── OrderSpecification.cs └── Utils │ └── TaskDelayer.cs ├── Readme.md ├── Samples ├── MauiSampleApp │ ├── App.xaml │ ├── App.xaml.cs │ ├── MauiProgram.cs │ ├── MauiSampleApp.csproj │ ├── Platforms │ │ ├── Android │ │ │ ├── AndroidManifest.xml │ │ │ ├── MainActivity.cs │ │ │ ├── MainApplication.cs │ │ │ └── Resources │ │ │ │ └── values │ │ │ │ └── colors.xml │ │ ├── Windows │ │ │ ├── App.xaml │ │ │ ├── App.xaml.cs │ │ │ ├── Package.appxmanifest │ │ │ └── app.manifest │ │ └── iOS │ │ │ ├── AppDelegate.cs │ │ │ ├── Info.plist │ │ │ └── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Resources │ │ ├── AppIcon │ │ │ ├── appicon.svg │ │ │ └── appiconfg.svg │ │ ├── Fonts │ │ │ ├── OpenSans-Regular.ttf │ │ │ └── OpenSans-Semibold.ttf │ │ ├── Images │ │ │ └── dotnet_bot.png │ │ ├── Raw │ │ │ └── AboutAssets.txt │ │ ├── Splash │ │ │ └── splash.svg │ │ └── Styles │ │ │ ├── Colors.xaml │ │ │ └── Styles.xaml │ └── Views │ │ ├── ItemTemplates │ │ ├── MallItemTemplate.xaml │ │ └── MallItemTemplate.xaml.cs │ │ ├── MainPage.xaml │ │ └── MainPage.xaml.cs ├── ObservableViewSample │ ├── Model │ │ └── Mall.cs │ ├── ObservableViewSample.csproj │ ├── Service │ │ ├── IMallManager.cs │ │ └── MallManager.cs │ └── ViewModel │ │ └── MainViewModel.cs ├── Settings.XamlStyler └── WpfSampleApp │ ├── App.xaml │ ├── App.xaml.cs │ ├── AssemblyInfo.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── ViewModels │ ├── FilterItemViewModel.cs │ └── FilterViewModel.cs │ ├── Views │ ├── FilterControl.xaml │ ├── FilterControl.xaml.cs │ ├── FilterItemView.xaml │ └── FilterItemView.xaml.cs │ └── WpfSampleApp.csproj ├── Tests └── ObservableView.Tests │ ├── Extensions │ ├── ExpressionExtensionsTests.cs │ ├── QueryableExtensionsTests.cs │ └── StringExtensionsTests.cs │ ├── GlobalUsings.cs │ ├── ObservableView.Tests.csproj │ ├── ObservableViewTests.cs │ ├── Searching │ ├── ExpressionBuilderTests.cs │ ├── Operands │ │ ├── ConstantOperandTests.cs │ │ ├── PropertyOperandTests.cs │ │ └── VariableOperandTests.cs │ ├── Operators │ │ ├── ContainsOperatorTests.cs │ │ └── EqualOperatorTests.cs │ ├── Processors │ │ └── ExpressionProcessorTests.cs │ └── SearchSpecificationTests.cs │ ├── Sorting │ └── NaturalSortComparableTests.cs │ ├── TestData │ ├── Car.cs │ ├── CarBrand.cs │ ├── CarPool.cs │ ├── Engine.cs │ ├── FuelType.cs │ ├── Person.cs │ └── TestHelper.cs │ └── TestHelpers │ └── UseCultureAttribute.cs ├── azure-pipelines.yml └── clean.bat /.editorconfig: -------------------------------------------------------------------------------- 1 | #################################################################### 2 | # Editor Configuration (Updated 2023-07-26) 3 | # 4 | # (c)2023 superdev GmbH 5 | #################################################################### 6 | 7 | root = true 8 | 9 | [*.csproj] 10 | indent_style = space 11 | indent_size = 2 12 | tab_width = 2 13 | 14 | [*.cs] 15 | indent_style = space 16 | indent_size = 4 17 | tab_width = 4 18 | end_of_line = crlf 19 | trim_trailing_whitespace = true 20 | insert_final_newline = false 21 | max_line_length = 140 22 | 23 | csharp_indent_block_contents = true 24 | csharp_indent_braces = false 25 | csharp_indent_case_contents = true 26 | csharp_indent_labels = one_less_than_current 27 | csharp_indent_switch_labels = true 28 | 29 | csharp_new_line_before_catch = true 30 | csharp_new_line_before_else = true 31 | csharp_new_line_before_finally = true 32 | csharp_new_line_before_members_in_anonymous_types = true 33 | csharp_new_line_before_members_in_object_initializers = true 34 | csharp_new_line_before_open_brace = all 35 | csharp_new_line_between_query_expression_clauses = true 36 | 37 | csharp_prefer_braces = true:error 38 | csharp_prefer_simple_default_expression = true:error 39 | csharp_prefer_simple_using_statement = false:silent 40 | 41 | csharp_using_directive_placement = outside_namespace:silent 42 | csharp_preserve_single_line_blocks = true 43 | csharp_preserve_single_line_statements = true 44 | 45 | csharp_space_after_cast = false 46 | csharp_space_after_colon_in_inheritance_clause = true 47 | csharp_space_after_comma = true 48 | csharp_space_after_dot = false 49 | csharp_space_after_keywords_in_control_flow_statements = true 50 | csharp_space_after_semicolon_in_for_statement = true 51 | csharp_space_around_binary_operators = before_and_after 52 | csharp_space_around_declaration_statements = do_not_ignore 53 | csharp_space_before_colon_in_inheritance_clause = true 54 | csharp_space_before_comma = false 55 | csharp_space_before_dot = false 56 | csharp_space_before_open_square_brackets = false 57 | csharp_space_before_semicolon_in_for_statement = false 58 | csharp_space_between_empty_square_brackets = false 59 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 60 | csharp_space_between_method_call_name_and_opening_parenthesis = false 61 | csharp_space_between_method_call_parameter_list_parentheses = false 62 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 63 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 64 | csharp_space_between_method_declaration_parameter_list_parentheses = false 65 | csharp_space_between_parentheses = none 66 | csharp_space_between_square_brackets = false 67 | 68 | # Expression-bodied members 69 | csharp_style_expression_bodied_accessors = true:none 70 | csharp_style_expression_bodied_constructors = false:none 71 | csharp_style_expression_bodied_indexers = true:none 72 | csharp_style_expression_bodied_lambdas = true:none 73 | csharp_style_expression_bodied_local_functions = false:silent 74 | csharp_style_expression_bodied_methods = false:none 75 | csharp_style_expression_bodied_operators = false:none 76 | csharp_style_expression_bodied_properties = true:silent 77 | 78 | csharp_style_conditional_delegate_call = true:error 79 | csharp_style_inlined_variable_declaration = true:error 80 | csharp_style_pattern_matching_over_as_with_null_check = true:error 81 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 82 | csharp_style_throw_expression = true:suggestion 83 | csharp_style_var_elsewhere = true:suggestion 84 | csharp_style_var_for_built_in_types = true:suggestion 85 | csharp_style_var_when_type_is_apparent = true:error 86 | csharp_style_implicit_object_creation_when_type_is_apparent = false 87 | csharp_style_prefer_switch_expression = false 88 | csharp_style_namespace_declarations = block_scoped:silent 89 | csharp_style_prefer_method_group_conversion = true:silent 90 | csharp_style_prefer_top_level_statements = true:silent 91 | csharp_style_prefer_null_check_over_type_check = true:suggestion 92 | 93 | dotnet_sort_system_directives_first = true 94 | dotnet_style_coalesce_expression = true:suggestion 95 | dotnet_style_collection_initializer = true:error 96 | dotnet_style_explicit_tuple_names = true:error 97 | dotnet_style_null_propagation = true:error 98 | dotnet_style_object_initializer = true:none 99 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 100 | dotnet_style_predefined_type_for_member_access = true:error 101 | 102 | dotnet_style_qualification_for_event = true:error 103 | dotnet_style_qualification_for_field = true:error 104 | dotnet_style_qualification_for_method = true:error 105 | dotnet_style_qualification_for_property = true:error 106 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 107 | dotnet_style_prefer_auto_properties = true:silent 108 | dotnet_style_prefer_simplified_boolean_expressions = false:suggestion 109 | dotnet_style_prefer_conditional_expression_over_assignment = false:suggestion 110 | dotnet_style_prefer_conditional_expression_over_return = false:suggestion 111 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 112 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 113 | dotnet_style_prefer_compound_assignment = true:suggestion 114 | dotnet_style_prefer_simplified_interpolation = true:suggestion 115 | dotnet_style_namespace_match_folder = true:suggestion 116 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 117 | csharp_style_unused_value_expression_statement_preference = discard_variable:none 118 | csharp_style_unused_value_assignment_preference = discard_variable:none 119 | csharp_style_prefer_method_group_conversion = true 120 | 121 | dotnet_naming_symbols.private_field_symbol.applicable_kinds = field 122 | dotnet_naming_symbols.private_field_symbol.applicable_accessibilities = private 123 | dotnet_naming_style.private_field_style.capitalization = camel_case 124 | dotnet_naming_rule.private_fields_are_camel_case.severity = error 125 | dotnet_naming_rule.private_fields_are_camel_case.symbols = private_field_symbol 126 | dotnet_naming_rule.private_fields_are_camel_case.style = private_field_style 127 | 128 | dotnet_naming_symbols.non_private_field_symbol.applicable_kinds = field 129 | dotnet_naming_symbols.non_private_field_symbol.applicable_accessibilities = public,internal,friend,protected,protected_internal,protected_friend 130 | dotnet_naming_style.non_private_field_style.capitalization = pascal_case 131 | dotnet_naming_rule.non_private_fields_are_pascal_case.severity = error 132 | dotnet_naming_rule.non_private_fields_are_pascal_case.symbols = non_private_field_symbol 133 | dotnet_naming_rule.non_private_fields_are_pascal_case.style = non_private_field_style 134 | 135 | dotnet_naming_symbols.parameter_symbol.applicable_kinds = parameter 136 | dotnet_naming_style.parameter_style.capitalization = camel_case 137 | dotnet_naming_rule.parameters_are_camel_case.severity = error 138 | dotnet_naming_rule.parameters_are_camel_case.symbols = parameter_symbol 139 | dotnet_naming_rule.parameters_are_camel_case.style = private_field_style 140 | 141 | dotnet_naming_symbols.non_interface_type_symbol.applicable_kinds = class,struct,enum,delegate 142 | dotnet_naming_style.non_interface_type_style.capitalization = pascal_case 143 | dotnet_naming_rule.non_interface_types_are_pascal_case.severity = error 144 | dotnet_naming_rule.non_interface_types_are_pascal_case.symbols = non_interface_type_symbol 145 | dotnet_naming_rule.non_interface_types_are_pascal_case.style = non_private_field_style 146 | 147 | dotnet_naming_symbols.interface_type_symbol.applicable_kinds = interface 148 | dotnet_naming_style.interface_type_style.capitalization = pascal_case 149 | dotnet_naming_style.interface_type_style.required_prefix = I 150 | dotnet_naming_rule.interface_types_must_be_prefixed_with_I.severity = error 151 | dotnet_naming_rule.interface_types_must_be_prefixed_with_I.symbols = interface_type_symbol 152 | dotnet_naming_rule.interface_types_must_be_prefixed_with_I.style = interface_type_style 153 | 154 | dotnet_naming_symbols.member_symbol.applicable_kinds = method,property,event 155 | dotnet_naming_style.member_style.capitalization = pascal_case 156 | dotnet_naming_rule.members_are_pascal_case.severity = error 157 | dotnet_naming_rule.members_are_pascal_case.symbols = member_symbol 158 | dotnet_naming_rule.members_are_pascal_case.style = non_private_field_style 159 | 160 | dotnet_naming_rule.static_fields_should_be_pascal_case.severity = suggestion 161 | dotnet_naming_rule.static_fields_should_be_pascal_case.symbols = static_fields 162 | dotnet_naming_rule.static_fields_should_be_pascal_case.style = non_private_field_style 163 | dotnet_naming_symbols.static_fields.applicable_kinds = field 164 | dotnet_naming_symbols.static_fields.applicable_accessibilities = * 165 | dotnet_naming_symbols.static_fields.required_modifiers = static 166 | dotnet_naming_style.static_field_style.capitalization = pascal_case 167 | 168 | # CS4014: Because this call is not awaited, execution of the current method continues before the call is completed 169 | dotnet_diagnostic.CS4014.severity = error 170 | 171 | # IDE0051: Remove unused private members 172 | dotnet_diagnostic.IDE0051.severity = warning -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: user?u=21232884 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Description 11 | 12 | ### Steps to Reproduce 13 | 14 | 1. 15 | 2. 16 | 3. 17 | 18 | ### Expected Behavior 19 | - 20 | 21 | ### Actual Behavior 22 | - 23 | 24 | ### Basic Information 25 | 26 | - Version with issue: 27 | - Last known good version: 28 | 29 | ### Screenshots 30 | 31 | 32 | 33 | ### Reproduction Link 34 | 35 | 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Enhancement]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Summary 11 | Please provide a brief summary of your proposal. Two to three sentences is best here. 12 | 13 | ## API Changes 14 | Include a list of all API changes, additions, subtractions as would be required by your proposal. 15 | 16 | e.g. 17 | 18 | In order for my new feature to be awesome, we need to create a new boolean value to indicate that it is awesome. 19 | 20 | ```csharp 21 | var myapi = new AwesomeApi(); 22 | myapi.beAwesome = true; 23 | ``` 24 | 25 | ## Intended Use Case 26 | Provide a detailed example of where your proposal would be used and for what purpose. 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Added / Fixed 2 | 3 | - Added a new feature 4 | - Fixed a bug 5 | 6 | ## Todo List 7 | 8 | - [ ] I have reviewed my changes 9 | - [ ] I have added unit tests 10 | - [ ] I have documented public APIs 11 | 12 | >*Todo list can be checked by putting a `X` inside the brackets* 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | *.bak 254 | *.p8 255 | *.p12 256 | 257 | # Firebase config files 258 | **/GoogleService-Info.plist 259 | **/google-services.json 260 | -------------------------------------------------------------------------------- /Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasgalliker/ObservableView/ee3f72dbf0384bd052755cb16f524bde95486167/Images/logo.png -------------------------------------------------------------------------------- /Images/logo_gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasgalliker/ObservableView/ee3f72dbf0384bd052755cb16f524bde95486167/Images/logo_gradient.png -------------------------------------------------------------------------------- /ObservableView/Exceptions/SearchTextPreprocessorException.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Exceptions 2 | { 3 | public class SearchTextPreprocessorException : Exception 4 | { 5 | public SearchTextPreprocessorException(string message, Exception innerException) 6 | : base(message, innerException) 7 | { 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /ObservableView/Extensions/EnumerableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Extensions 2 | { 3 | public static class EnumerableExtensions 4 | { 5 | /// 6 | /// To the observable collection. 7 | /// 8 | /// Generic type T. 9 | /// The enumerable. 10 | /// The resulting ObservableCollection<T>. 11 | public static ObservableCollection ToObservableCollection(this IEnumerable enumerable) 12 | { 13 | return new ObservableCollection(enumerable); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /ObservableView/Extensions/ExpressionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Extensions 2 | { 3 | public static class ExpressionExtensions 4 | { 5 | public static Expression ToLower(this Expression expression) 6 | { 7 | EnsureToString(ref expression); 8 | 9 | var methodInfo = ReflectionHelper.GetMethod(source => source.ToLower()); 10 | var toLowerExpression = Expression.Call(expression, methodInfo); 11 | 12 | expression = IsNotNull(expression, toLowerExpression); 13 | return expression; 14 | } 15 | 16 | public static Expression ToUpper(this Expression expression) 17 | { 18 | EnsureToString(ref expression); 19 | 20 | var methodInfo = ReflectionHelper.GetMethod(source => source.ToUpper()); 21 | var toUpperExpression = Expression.Call(expression, methodInfo); 22 | 23 | expression = IsNotNull(expression, toUpperExpression); 24 | return expression; 25 | } 26 | 27 | public static Expression Trim(this Expression expression) 28 | { 29 | EnsureToString(ref expression); 30 | 31 | var methodInfo = ReflectionHelper.GetMethod(source => source.Trim()); 32 | var trimExpression = Expression.Call(expression, methodInfo); 33 | 34 | expression = IsNotNull(expression, trimExpression); 35 | return expression; 36 | } 37 | 38 | public static Expression ToStringExpression(this Expression expression) 39 | { 40 | var methodInfo = ReflectionHelper.GetMethod(source => source.ToString()); 41 | return Expression.Call(expression, methodInfo); 42 | } 43 | 44 | private static void EnsureToString(ref Expression expression) 45 | { 46 | if (expression.Type != typeof(string)) 47 | { 48 | expression = expression.ToStringExpression(); 49 | } 50 | } 51 | 52 | public static Expression IsNotNull(this Expression checkExpression, Expression expressionIfNotNull) 53 | { 54 | var expression = Expression.Condition( 55 | Expression.NotEqual(checkExpression, Expression.Constant(null, checkExpression.Type)), 56 | expressionIfNotNull, 57 | Expression.Constant(string.Empty)); 58 | 59 | return expression; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /ObservableView/Extensions/ObservableViewExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETFRAMEWORK 2 | using System; 3 | using System.Collections.Specialized; 4 | using System.ComponentModel; 5 | using System.Linq; 6 | 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using ObservableView.Sorting; 11 | 12 | namespace ObservableView.Extensions 13 | { 14 | public static class ObservableViewExtensions 15 | { 16 | private static DataGrid dataGrid; 17 | 18 | public static readonly DependencyProperty ObservableViewProperty = DependencyProperty.RegisterAttached( 19 | "ObservableView", 20 | typeof(object), 21 | typeof(ObservableViewExtensions), 22 | new UIPropertyMetadata(null, ObservableViewPropertyChanged)); 23 | 24 | public static object GetObservableView(DependencyObject obj) 25 | { 26 | return obj.GetValue(ObservableViewProperty); 27 | } 28 | 29 | public static void SetObservableView(DependencyObject obj, object value) 30 | { 31 | obj.SetValue(ObservableViewProperty, value); 32 | } 33 | 34 | private static void ObservableViewPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 35 | { 36 | dataGrid = d as DataGrid; 37 | if (dataGrid == null) 38 | { 39 | return; 40 | } 41 | 42 | if (e.OldValue is IObservableView oldObservableView) 43 | { 44 | dataGrid.Loaded -= DataGridLoaded; 45 | dataGrid.Unloaded -= DataGridUnloaded; 46 | } 47 | 48 | if (e.NewValue is IObservableView newObservableView) 49 | { 50 | dataGrid.Loaded += DataGridLoaded; 51 | dataGrid.Unloaded += DataGridUnloaded; 52 | 53 | CheckIfItemsSourcePropertyIsNotBound(); 54 | BindObservableViewToItemsSource(); 55 | } 56 | } 57 | 58 | static void SortDescriptionCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) 59 | { 60 | Console.WriteLine("SortDescriptionCollectionChanged: Action={0}", e.Action); 61 | 62 | if (dataGrid == null) 63 | { 64 | return; 65 | } 66 | 67 | if (GetObservableView(dataGrid) is not IObservableView observableView) 68 | { 69 | return; 70 | } 71 | 72 | if (e.Action == NotifyCollectionChangedAction.Reset) 73 | { 74 | // clear all columns sort directions 75 | foreach (var column in dataGrid.Columns) 76 | { 77 | column.SortDirection = null; 78 | } 79 | } 80 | 81 | if (e.NewItems != null) 82 | { 83 | observableView.ClearOrderSpecifications(); 84 | var allSortDescriptions = (SortDescriptionCollection)sender; 85 | 86 | // set columns sort directions 87 | var sortDescriptions = allSortDescriptions.Union(e.NewItems.Cast()).ToList(); 88 | foreach (SortDescription sortDescription in sortDescriptions) //TODO GATH NewItems only contains NEW ITEMS; We should also consider existing sort orders 89 | { 90 | 91 | var orderDirection = sortDescription.Direction == ListSortDirection.Ascending ? OrderDirection.Ascending : OrderDirection.Descending; 92 | observableView.AddOrderSpecification(sortDescription.PropertyName, orderDirection); 93 | 94 | SetSortDirection(sortDescription.PropertyName, sortDescription.Direction); 95 | } 96 | } 97 | 98 | if (e.OldItems != null) 99 | { 100 | // reset columns sort directions 101 | foreach (SortDescription descr in e.OldItems) 102 | { 103 | observableView.RemoveOrderSpecification(descr.PropertyName); 104 | SetSortDirection(descr.PropertyName, null); 105 | } 106 | } 107 | } 108 | 109 | private static void SetSortDirection(string sortMemberPath, ListSortDirection? direction) 110 | { 111 | var column = dataGrid.Columns.FirstOrDefault(c => c.SortMemberPath == sortMemberPath); 112 | if (column != null) 113 | { 114 | column.SortDirection = direction; 115 | } 116 | } 117 | 118 | /// 119 | /// Check if there is a binding to ItemsSource. 120 | /// If you use the ObservableView dependency property, you must use ItemsSource at the same time. 121 | /// 122 | private static void CheckIfItemsSourcePropertyIsNotBound() 123 | { 124 | var itemsSourceBindingExpression = dataGrid.GetBindingExpression(ItemsControl.ItemsSourceProperty); 125 | if (itemsSourceBindingExpression != null) 126 | { 127 | throw new InvalidOperationException("Dependency property 'ItemsSource' must not have a binding for ObservableView to work properly. " + 128 | "Bind to ObservableView instead."); 129 | } 130 | } 131 | 132 | /// 133 | /// Since we want to bind the ObservableView directly to the ObservableViewProperty, 134 | /// we have to make sure that the ItemsSourceProperty of the DataGrid is bound programmatically to the ObservableView.View property. 135 | /// 136 | private static void BindObservableViewToItemsSource() 137 | { 138 | var observableViewBindingExpression = dataGrid.GetBindingExpression(ObservableViewProperty); 139 | if (observableViewBindingExpression == null) 140 | { 141 | throw new InvalidOperationException("Dependency property 'ObservableView' does not have a valid binding."); 142 | } 143 | 144 | string viewPropertyBindingName = observableViewBindingExpression.ResolvedSourcePropertyName + ".View"; 145 | var viewPropertyBinding = new Binding(viewPropertyBindingName); 146 | dataGrid.SetBinding(ItemsControl.ItemsSourceProperty, viewPropertyBinding); 147 | } 148 | 149 | private static void DataGridUnloaded(object sender, RoutedEventArgs e) 150 | { 151 | if (dataGrid == null) 152 | { 153 | return; 154 | } 155 | 156 | if (dataGrid.Items.SortDescriptions is INotifyCollectionChanged notifyCollectionChanged) 157 | { 158 | notifyCollectionChanged.CollectionChanged -= SortDescriptionCollectionChanged; 159 | } 160 | 161 | if (GetObservableView(dataGrid) is not IObservableView observableView) 162 | { 163 | return; 164 | } 165 | 166 | observableView.PropertyChanged -= ObservableViewOnPropertyChanged; 167 | } 168 | 169 | private static void DataGridLoaded(object sender, RoutedEventArgs e) 170 | { 171 | if (dataGrid == null) 172 | { 173 | return; 174 | } 175 | 176 | if (GetObservableView(dataGrid) is not IObservableView observableView) 177 | { 178 | return; 179 | } 180 | 181 | observableView.PropertyChanged += ObservableViewOnPropertyChanged; 182 | 183 | // initial sync 184 | SyncObservableViewSortWithDataGridSort(observableView, isInitialSync: true); 185 | 186 | // Subscribe column sort changed 187 | if (dataGrid.Items.SortDescriptions is INotifyCollectionChanged notifyCollectionChanged) 188 | { 189 | notifyCollectionChanged.CollectionChanged += SortDescriptionCollectionChanged; 190 | } 191 | } 192 | 193 | private static void ObservableViewOnPropertyChanged(object sender, PropertyChangedEventArgs args) 194 | { 195 | if (args.PropertyName == "View") 196 | { 197 | if (sender is not IObservableView observableView) 198 | { 199 | return; 200 | } 201 | 202 | SyncObservableViewSortWithDataGridSort(observableView); 203 | } 204 | } 205 | 206 | /// 207 | /// This method is used to synchronize the ObservableView's sort specification 208 | /// with the DataGrid's sort specification. 209 | /// This is the case if the ObservableView refreshes its data. 210 | /// (For some mysterious reasons, the DataGrid loses its sort specification in this case) 211 | /// 212 | private static void SyncObservableViewSortWithDataGridSort(IObservableView observableView, bool isInitialSync = false) 213 | { 214 | Console.WriteLine("Sync ObservableView => DataGrid"); 215 | 216 | if (isInitialSync) 217 | { 218 | dataGrid.Items.SortDescriptions.Clear(); 219 | } 220 | 221 | foreach (var dataGridColumn in dataGrid.Columns) 222 | { 223 | var listSortDirection = observableView.GetSortSpecification(dataGridColumn.SortMemberPath).ToSortDirection(); 224 | if (listSortDirection != null) 225 | { 226 | dataGridColumn.SortDirection = listSortDirection; 227 | if (isInitialSync) 228 | { 229 | dataGrid.Items.SortDescriptions.Add(new SortDescription(dataGridColumn.SortMemberPath, listSortDirection.Value)); 230 | } 231 | } 232 | } 233 | } 234 | } 235 | } 236 | #endif -------------------------------------------------------------------------------- /ObservableView/Extensions/OperationExtensions.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | using ObservableView.Searching.Operations; 3 | 4 | namespace ObservableView.Extensions 5 | { 6 | internal static class OperationExtensions 7 | { 8 | internal static IEnumerable Flatten(this Operation operation) 9 | { 10 | return Recurse(operation); 11 | } 12 | 13 | private static IEnumerable Recurse(object obj) 14 | { 15 | if (obj is BinaryOperation binaryOperation) 16 | { 17 | yield return binaryOperation.LeftOperand; 18 | yield return binaryOperation.Operator; 19 | yield return binaryOperation.RightOperand; 20 | } 21 | 22 | if (obj is GroupOperation groupOperation) 23 | { 24 | foreach (var groupObject in RecurseGroupOperation(groupOperation)) 25 | { 26 | yield return groupObject; 27 | } 28 | } 29 | } 30 | 31 | private static IEnumerable RecurseGroupOperation(GroupOperation groupOperation) 32 | { 33 | foreach (var binObject in Recurse(groupOperation.LeftOperation)) 34 | { 35 | yield return binObject; 36 | } 37 | 38 | yield return groupOperation.Operator; 39 | 40 | foreach (var binObject in Recurse(groupOperation.RightOperation)) 41 | { 42 | yield return binObject; 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /ObservableView/Extensions/QueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Extensions 2 | { 3 | [Preserve(AllMembers = true)] 4 | public static class QueryableExtensions 5 | { 6 | public static IQueryable Where(this IQueryable source, Expression baseExpression, ParameterExpression parameterExpression) 7 | { 8 | if (source == null) 9 | { 10 | throw new ArgumentNullException("source"); 11 | } 12 | if (baseExpression == null) 13 | { 14 | throw new ArgumentNullException("baseExpression"); 15 | } 16 | if (parameterExpression == null) 17 | { 18 | throw new ArgumentNullException("parameterExpression"); 19 | } 20 | 21 | // TODO: Use ReflectionHelper here 22 | ////MethodInfo whereMethodInfo = ReflectionHelper>.GetMethod>((x, arg) => x.Where(arg)); 23 | ////MethodInfo genericWhereMethodInfo = whereMethodInfo 24 | //// .GetGenericMethodDefinition() 25 | //// .MakeGenericMethod(source.ElementType); 26 | 27 | ////MethodCallExpression whereCallExpression = Expression.Call( 28 | //// null, 29 | //// genericWhereMethodInfo, 30 | //// source.Expression, 31 | //// Expression.Lambda>(baseExpression, new[] { parameterExpression })); 32 | 33 | MethodCallExpression whereCallExpression = Expression.Call( 34 | typeof(Queryable), 35 | nameof(System.Linq.Queryable.Where), 36 | new[] { source.ElementType }, 37 | source.Expression, 38 | Expression.Lambda>(baseExpression, new[] { parameterExpression })); 39 | 40 | return source.Provider.CreateQuery(whereCallExpression); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /ObservableView/Extensions/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Extensions 2 | { 3 | internal static class ReflectionHelper 4 | { 5 | /// 6 | /// Gets the method represented by the lambda expression. 7 | /// 8 | /// An expression that invokes a method. 9 | /// The is null. 10 | /// The does not represent a method call. 11 | /// The method info represented by the lambda expression. 12 | internal static MethodInfo GetMethod(Expression> methodSelector) 13 | { 14 | return GetMethodInfo(methodSelector); 15 | } 16 | 17 | /// 18 | /// Gets the method represented by the lambda expression. 19 | /// 20 | /// An expression that invokes a method. 21 | /// Type of the first argument. 22 | /// The is null. 23 | /// The does not represent a method call. 24 | /// The method info represented by the lambda expression. 25 | internal static MethodInfo GetMethod(Expression> methodSelector) 26 | { 27 | return GetMethodInfo(methodSelector); 28 | } 29 | 30 | /// 31 | /// Gets the method represented by the lambda expression. 32 | /// 33 | /// An expression that invokes a method. 34 | /// Type of the first argument. 35 | /// Type of the second argument. 36 | /// The is null. 37 | /// The does not represent a method call. 38 | /// The method info represented by the lambda expression. 39 | internal static MethodInfo GetMethod(Expression> methodSelector) 40 | { 41 | return GetMethodInfo(methodSelector); 42 | } 43 | 44 | /// 45 | /// Gets the method represented by the lambda expression. 46 | /// 47 | /// An expression that invokes a method. 48 | /// Type of the first argument. 49 | /// Type of the second argument. 50 | /// Type of the third argument. 51 | /// The is null. 52 | /// The does not represent a method call. 53 | /// The method info represented by the lambda expression. 54 | internal static MethodInfo GetMethod(Expression> methodSelector) 55 | { 56 | return GetMethodInfo(methodSelector); 57 | } 58 | 59 | /// 60 | /// Gets the property represented by the lambda expression. 61 | /// 62 | /// The type of the property. 63 | /// An expression that accesses a property. 64 | /// The property info represented by the lambda expression. 65 | /// The is null. 66 | /// The does not represent a property access. 67 | internal static PropertyInfo GetProperty(Expression> propertySelector) 68 | { 69 | PropertyInfo info = GetMemberInfo(propertySelector) as PropertyInfo; 70 | 71 | return info; 72 | } 73 | 74 | /// 75 | /// Gets the field represented by the lambda expression. 76 | /// 77 | /// The type of the field. 78 | /// An expression that accesses a field. 79 | /// The field info represented by the lambda expression. 80 | /// The is null. 81 | /// The does not represent a field access. 82 | internal static FieldInfo GetField(Expression> fieldSelector) 83 | { 84 | FieldInfo info = GetMemberInfo(fieldSelector) as FieldInfo; 85 | 86 | return info; 87 | } 88 | 89 | /// 90 | /// Gets the method info represented by the lambda expression. 91 | /// 92 | /// An expression that invokes a method. 93 | /// The is null. 94 | /// The does not represent a method call. 95 | /// The method info represented by the lambda expression. 96 | private static MethodInfo GetMethodInfo(LambdaExpression methodSelector) 97 | { 98 | MethodCallExpression callExpression = methodSelector.Body as MethodCallExpression; 99 | return callExpression.Method; 100 | } 101 | 102 | /// 103 | /// Gets the member info represented by the lambda expression. 104 | /// 105 | /// An expression that accesses a member. 106 | /// The member info represented by the lambda expression. 107 | /// The is null. 108 | /// The does not represent a member access. 109 | private static MemberInfo GetMemberInfo(LambdaExpression lambdaExpression) 110 | { 111 | var memberExpression = GetMemberExpression(lambdaExpression); 112 | return memberExpression.Member; 113 | } 114 | 115 | private static MemberExpression GetMemberExpression(LambdaExpression lambdaExpression) 116 | { 117 | var member = lambdaExpression.Body as MemberExpression; 118 | var unary = lambdaExpression.Body as UnaryExpression; 119 | var memberExpression = member ?? (unary != null ? unary.Operand as MemberExpression : null); 120 | 121 | if (memberExpression == null) 122 | { 123 | throw new ArgumentException("'lambdaExpression' should be a member expression"); 124 | } 125 | 126 | return memberExpression; 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /ObservableView/Extensions/SortDirectionExtensions.cs: -------------------------------------------------------------------------------- 1 | #if NETFRAMEWORK 2 | using System.ComponentModel; 3 | using ObservableView.Sorting; 4 | 5 | namespace ObservableView.Extensions 6 | { 7 | public static class SortDirectionExtensions 8 | { 9 | public static ListSortDirection? ToSortDirection(this OrderDirection? orderDirection) 10 | { 11 | if (orderDirection == null) 12 | { 13 | return null; 14 | } 15 | 16 | if (orderDirection == OrderDirection.Descending) 17 | { 18 | return ListSortDirection.Descending; 19 | } 20 | 21 | return ListSortDirection.Ascending; 22 | } 23 | 24 | public static OrderDirection? ToSortDirection(this ListSortDirection? listSortDirection) 25 | { 26 | if (listSortDirection == null) 27 | { 28 | return null; 29 | } 30 | 31 | if (listSortDirection == ListSortDirection.Descending) 32 | { 33 | return OrderDirection.Descending; 34 | } 35 | 36 | return OrderDirection.Ascending; 37 | } 38 | } 39 | } 40 | #endif -------------------------------------------------------------------------------- /ObservableView/Extensions/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Extensions 2 | { 3 | public static class StringExtensions 4 | { 5 | public static bool Contains(this string source, string toCheck, StringComparison comp) 6 | { 7 | return source.IndexOf(toCheck, comp) >= 0; 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /ObservableView/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Extensions 2 | { 3 | internal static class TypeExtensions 4 | { 5 | public static Type GetGenericType(this Type type) 6 | { 7 | if (type.IsNullable()) 8 | { 9 | type = type.GetTypeInfo().GenericTypeArguments[0]; 10 | } 11 | return type; 12 | } 13 | 14 | public static bool IsNullable(this Type type) 15 | { 16 | return type != null && type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); 17 | } 18 | 19 | public static Expression GetDefaultValueExpression(this Type type) 20 | { 21 | Expression defaultValueExpression = null; 22 | if (type.GetTypeInfo().IsValueType) // This distinction is obsolete but helps speeding up at execution time 23 | { 24 | defaultValueExpression = Expression.Default(type); 25 | } 26 | else 27 | { 28 | defaultValueExpression = Expression.Constant(null, type); 29 | } 30 | 31 | return defaultValueExpression; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /ObservableView/Filtering/FilterEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Filtering 2 | { 3 | public class FilterEventArgs : EventArgs 4 | { 5 | public FilterEventArgs(T item) 6 | { 7 | this.Item = item; 8 | this.IsAllowed = true; 9 | } 10 | 11 | /// 12 | /// Gets or sets a value indicating whether this instance is allowed 13 | /// Allowed means this item is included in the view returned in ObservableView.View property. 14 | /// 15 | /// 16 | /// true if this instance is allowed; otherwise, false. 17 | /// 18 | public bool IsAllowed { get; set; } 19 | 20 | /// 21 | /// Gets the current item of the collection. 22 | /// 23 | /// 24 | /// The item. 25 | /// 26 | public T Item { get; private set; } 27 | } 28 | } -------------------------------------------------------------------------------- /ObservableView/Filtering/FilterEventHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace ObservableView.Filtering 3 | { 4 | public delegate void FilterEventHandler(object sender, FilterEventArgs e); 5 | } 6 | -------------------------------------------------------------------------------- /ObservableView/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.Collections.ObjectModel; 4 | global using System.Collections.Specialized; 5 | global using System.Linq; 6 | global using System.Linq.Expressions; 7 | global using System.Reflection; 8 | global using ObservableView.Exceptions; 9 | global using ObservableView.Extensions; 10 | global using ObservableView.Filtering; 11 | global using ObservableView.Grouping; 12 | global using ObservableView.Searching; 13 | global using ObservableView.Sorting; 14 | -------------------------------------------------------------------------------- /ObservableView/Grouping/AlphaGroupKeyAlgorithm.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Grouping 2 | { 3 | public class AlphaGroupKeyAlgorithm : GroupKeyAlgorithm 4 | { 5 | private readonly bool upperCase; 6 | 7 | public AlphaGroupKeyAlgorithm(bool upperCase = true) 8 | { 9 | this.upperCase = upperCase; 10 | } 11 | 12 | public override string GetGroupKey(string value) 13 | { 14 | if (value == null) 15 | { 16 | return null; 17 | } 18 | 19 | if (value == "") 20 | { 21 | return ""; 22 | } 23 | 24 | char firstChar = value[0]; 25 | if (char.IsNumber(firstChar)) 26 | { 27 | return "#"; 28 | } 29 | 30 | if (this.upperCase) 31 | { 32 | return firstChar.ToString().ToUpper(); 33 | } 34 | 35 | return firstChar.ToString().ToLower(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ObservableView/Grouping/GroupKeyAlgorithm.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Grouping 2 | { 3 | public abstract class GroupKeyAlgorithm : IGroupKeyAlgorithm, IGroupKeyAlgorithm 4 | { 5 | public string GetGroupKey(object item) 6 | { 7 | return this.GetGroupKey((T)item); 8 | } 9 | 10 | public abstract string GetGroupKey(T value); 11 | } 12 | } -------------------------------------------------------------------------------- /ObservableView/Grouping/Grouping.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ObservableView.Grouping 4 | { 5 | [DebuggerDisplay("Key = {Key}, Count = {this.Items.Count}")] 6 | public class Grouping : ObservableCollection 7 | { 8 | public string Key { get; private set; } 9 | 10 | public Grouping(string key, IEnumerable items) 11 | { 12 | this.Key = key; 13 | foreach (var item in items) 14 | { 15 | this.Items.Add(item); 16 | } 17 | } 18 | 19 | public override string ToString() 20 | { 21 | return string.Format("Key={0}, Count={1}", this.Key, this.Items.Count); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /ObservableView/Grouping/IGroupKeyAlgorithm.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace ObservableView.Grouping 3 | { 4 | public interface IGroupKeyAlgorithm : IGroupKeyAlgorithm 5 | { 6 | string GetGroupKey(T value); 7 | } 8 | 9 | public interface IGroupKeyAlgorithm 10 | { 11 | string GetGroupKey(object item); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ObservableView/Grouping/MonthGroupAlgorithm.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Grouping 2 | { 3 | public class MonthGroupAlgorithm : GroupKeyAlgorithm 4 | { 5 | private readonly Func nullString; 6 | 7 | public MonthGroupAlgorithm(Func nullString = null) 8 | { 9 | if (nullString == null) 10 | { 11 | nullString = () => string.Empty; 12 | } 13 | 14 | this.nullString = nullString; 15 | } 16 | 17 | public override string GetGroupKey(DateTime? value) 18 | { 19 | if (!value.HasValue) 20 | { 21 | return this.nullString(); 22 | } 23 | 24 | var dateTime = value.Value; 25 | var str = dateTime.ToString("MMMM"); 26 | if (DateTime.Today.Year == dateTime.Year) 27 | { 28 | return str; 29 | } 30 | 31 | return str + " " + dateTime.Year; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /ObservableView/IObservableView.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace ObservableView 4 | { 5 | internal interface IObservableView 6 | { 7 | event PropertyChangedEventHandler PropertyChanged; 8 | 9 | OrderDirection? GetSortSpecification(string propertyName); 10 | 11 | /// 12 | /// Adds a new order specification for a certain property of given propertyName. 13 | /// 14 | /// The property name of the column which shall be sorted. 15 | /// Order direction in which the selected property shall be sorted. 16 | void AddOrderSpecification(string propertyName, OrderDirection orderDirection = OrderDirection.Ascending); 17 | 18 | void RemoveOrderSpecification(string propertyName); 19 | 20 | /// 21 | /// Removes all order specifications. 22 | /// 23 | void ClearOrderSpecifications(); 24 | } 25 | } -------------------------------------------------------------------------------- /ObservableView/ObservableObject.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace ObservableView 5 | { 6 | /// 7 | /// Implementation of . 8 | /// 9 | [Preserve(AllMembers = true)] 10 | [EditorBrowsable(EditorBrowsableState.Never)] 11 | public abstract class BindableBase : INotifyPropertyChanged 12 | { 13 | public event PropertyChangedEventHandler PropertyChanged; 14 | 15 | protected bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null) 16 | { 17 | if (EqualityComparer.Default.Equals(storage, value)) 18 | { 19 | return false; 20 | } 21 | 22 | storage = value; 23 | this.OnPropertyChanged(propertyName); 24 | 25 | return true; 26 | } 27 | 28 | protected void OnPropertyChanged([CallerMemberName] string propertyName = null) 29 | { 30 | this.OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); 31 | } 32 | 33 | private void OnPropertyChanged(PropertyChangedEventArgs args) 34 | { 35 | this.PropertyChanged?.Invoke(this, args); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /ObservableView/ObservableView.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Library 6 | disable 7 | true 8 | latest 9 | True 10 | 11 | 12 | 13 | 14 | ObservableView 15 | ObservableView 16 | 1.0.0 17 | Thomas Galliker 18 | ObservableView 19 | ObservableView;ObservableCollection;sorting;filtering;searching;collections;xamarin 20 | https://raw.githubusercontent.com/thomasgalliker/ObservableView/develop/Images/logo.png 21 | https://github.com/thomasgalliker/ObservableView 22 | git 23 | https://github.com/thomasgalliker/ObservableView 24 | superdev GmbH 25 | false 26 | 3.0.0 27 | - Drop support for .NET framework 28 | - Drop support for Xamarin 29 | 30 | 2.0.0 31 | - Support for .Net Standard 2.0 32 | 33 | 1.1.0-pre1 34 | - Complete overwork of the search API 35 | - Added custom SearchTextDelimiters 36 | - Added SearchTextPreprocessor 37 | - Added configurable SearchTextLogic (AND, OR) 38 | 39 | 1.0.2 40 | - Bug fix: Sorting in WPF DataGrid headers not reflected correctly 41 | - Bug fix: Sorting not updated on View property refresh 42 | 43 | 1.0.1 44 | - Initial release 45 | 46 | Copyright $([System.DateTime]::Now.ToString(`yyyy`)) © Thomas Galliker 47 | README.md 48 | 49 | 50 | 51 | 52 | True 53 | \ 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /ObservableView/PreserveAttribute.cs: -------------------------------------------------------------------------------- 1 | [assembly: Preserve(AllMembers = true)] 2 | 3 | [AttributeUsage( 4 | AttributeTargets.Assembly 5 | | AttributeTargets.Class 6 | | AttributeTargets.Struct 7 | | AttributeTargets.Enum 8 | | AttributeTargets.Constructor 9 | | AttributeTargets.Method 10 | | AttributeTargets.Property 11 | | AttributeTargets.Field 12 | | AttributeTargets.Event 13 | | AttributeTargets.Interface 14 | | AttributeTargets.Delegate, 15 | AllowMultiple = true)] 16 | public sealed class PreserveAttribute : Attribute 17 | { 18 | 19 | public bool AllMembers; 20 | public bool Conditional; 21 | 22 | public PreserveAttribute() 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /ObservableView/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | #if NET 4 | [assembly: XmlnsDefinition("http://observableview", "ObservableView")] 5 | [assembly: XmlnsDefinition("http://observableview", "ObservableView.Extensions")] 6 | #endif 7 | 8 | [assembly: InternalsVisibleTo("ObservableView.Tests")] -------------------------------------------------------------------------------- /ObservableView/SearchLogic.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView 2 | { 3 | public enum SearchLogic 4 | { 5 | And, 6 | Or 7 | } 8 | } -------------------------------------------------------------------------------- /ObservableView/Searching/ExpressionBuilder.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | 3 | namespace ObservableView.Searching 4 | { 5 | public class ExpressionBuilder : IExpressionBuilder 6 | { 7 | /// 8 | /// Creates a new instance of ExpressionBuilder. 9 | /// 10 | /// The type for which the ParameterExpression will be created. 11 | public ExpressionBuilder(Type parameterType) 12 | { 13 | var name = parameterType.Name.ToCharArray(); 14 | var capitalLetters = name.Where(char.IsUpper).Select(x => x.ToString()); 15 | var parameterName = capitalLetters.Aggregate((a, b) => a + b).ToLower(); 16 | 17 | this.ParameterExpression = Expression.Parameter(parameterType, parameterName); 18 | } 19 | 20 | /// 21 | /// Creates a new instance of ExpressionBuilder. 22 | /// 23 | /// Represents a named parameter expression. 24 | public ExpressionBuilder(ParameterExpression parameterExpression) 25 | { 26 | this.ParameterExpression = parameterExpression; 27 | } 28 | 29 | public ParameterExpression ParameterExpression { get; } 30 | 31 | public Expression Build(Operation operation) 32 | { 33 | return operation.Operator.Build(this, operation); 34 | } 35 | 36 | public Expression Build(IOperand operand) 37 | { 38 | return operand.Build(this); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /ObservableView/Searching/IExpressionBuilder.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | 3 | namespace ObservableView.Searching 4 | { 5 | /// 6 | /// ExpressionBuilder provides a 7 | /// to build expressions for filter operations. 8 | /// 9 | public interface IExpressionBuilder 10 | { 11 | /// 12 | /// Gets parameter expression. 13 | /// 14 | /// The parameter expression. 15 | ParameterExpression ParameterExpression { get; } 16 | 17 | /// 18 | /// Builds the expression for the specified filter operation. 19 | /// 20 | /// The filter operation. 21 | /// The built expression. 22 | /// The parameter is null. 23 | Expression Build(Operation operation); 24 | 25 | /// 26 | /// Builds the expression for the specified filter operand. 27 | /// 28 | /// The filter operand. 29 | /// The built expression. 30 | /// The parameter is null. 31 | Expression Build(IOperand operand); 32 | } 33 | } -------------------------------------------------------------------------------- /ObservableView/Searching/ISearchSpecification.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | using ObservableView.Searching.Operators; 3 | using ObservableView.Searching.Processors; 4 | 5 | namespace ObservableView.Searching 6 | { 7 | public interface ISearchSpecification 8 | { 9 | event EventHandler SearchSpecificationAdded; 10 | 11 | event EventHandler SearchSpecificationsCleared; 12 | 13 | ISearchSpecification Add(Expression> propertyExpression, BinaryOperator @operator = null); 14 | 15 | ISearchSpecification Add(Expression> propertyExpression, IExpressionProcessor[] expressionProcessors, BinaryOperator @operator = null); 16 | 17 | ISearchSpecification And(Expression> propertyExpression, BinaryOperator @operator = null); 18 | 19 | ISearchSpecification And(Expression> propertyExpression, IExpressionProcessor[] expressionProcessors, BinaryOperator @operator = null); 20 | 21 | ISearchSpecification Or(Expression> propertyExpression, BinaryOperator @operator = null); 22 | 23 | ISearchSpecification Or(Expression> propertyExpression, IExpressionProcessor[] expressionProcessors, BinaryOperator @operator = null); 24 | 25 | void ReplaceSearchTextVariables(TX value); 26 | 27 | Operation BaseOperation { get; } 28 | 29 | /// 30 | /// Removes all search specifications and resets SearchText to string.Empty. 31 | /// 32 | void Clear(); 33 | 34 | /// 35 | /// Checks if there are any search specifications defined. 36 | /// 37 | /// 38 | bool Any(); 39 | } 40 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operands/ConstantOperand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Globalization; 3 | 4 | namespace ObservableView.Searching.Operands 5 | { 6 | [DebuggerDisplay("ConstantOperand: Value={Value}, Type={Type}")] 7 | public class ConstantOperand : Operand 8 | { 9 | private object value; 10 | 11 | /// 12 | /// Takes a target type. The value is not (yet) specified. 13 | /// 14 | public ConstantOperand(Type type) 15 | { 16 | this.Type = type; 17 | } 18 | 19 | /// 20 | /// Takes a value of arbitrary data. 21 | /// 22 | public ConstantOperand(object value) 23 | { 24 | if (value == null) 25 | { 26 | throw new ArgumentNullException("value"); 27 | } 28 | 29 | this.Value = value; 30 | this.Type = value.GetType(); 31 | } 32 | 33 | /// 34 | /// Takes a value of arbitrary data 35 | /// which will be converted to type at build time. 36 | /// 37 | public ConstantOperand(object value, Type type) 38 | : this(value) 39 | { 40 | this.Type = type; 41 | } 42 | 43 | public Type Type { get; private set; } 44 | 45 | public object Value 46 | { 47 | get 48 | { 49 | return this.value; 50 | } 51 | set 52 | { 53 | this.value = value; 54 | this.Type = value == null ? typeof(object) : value.GetType(); 55 | } 56 | } 57 | 58 | public override Expression Build(IExpressionBuilder expressionBuilder) 59 | { 60 | Expression constantExpression = null; 61 | 62 | var genericType = this.Type.GetGenericType(); 63 | var convertedValue = TryConvertToTargetType(this.Value, targetType: genericType); 64 | if (convertedValue != null) 65 | { 66 | constantExpression = Expression.Constant(convertedValue, this.Type); 67 | } 68 | else 69 | { 70 | constantExpression = this.Type.GetDefaultValueExpression(); 71 | } 72 | 73 | return constantExpression; 74 | } 75 | 76 | private static object TryConvertToTargetType(object value, Type targetType) 77 | { 78 | if (targetType != null && value != null) 79 | { 80 | try 81 | { 82 | if (targetType.GetTypeInfo().IsEnum) 83 | { 84 | value = Enum.ToObject(targetType, value); 85 | } 86 | else 87 | { 88 | value = Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture); 89 | } 90 | } 91 | catch (FormatException formatException) 92 | { 93 | throw new FormatException( 94 | string.Format(CultureInfo.InvariantCulture, "Value {0} does not match type {1}.", 95 | value, 96 | targetType), 97 | formatException); 98 | } 99 | } 100 | 101 | return value; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operands/IOperand.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Processors; 2 | 3 | namespace ObservableView.Searching.Operands 4 | { 5 | public interface IOperand 6 | { 7 | Expression Build(IExpressionBuilder expressionBuilder); 8 | 9 | IExpressionProcessor[] ExpressionProcessors { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operands/Operand.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Processors; 2 | 3 | namespace ObservableView.Searching.Operands 4 | { 5 | public abstract class Operand : IOperand 6 | { 7 | public abstract Expression Build(IExpressionBuilder expressionBuilder); 8 | 9 | public IExpressionProcessor[] ExpressionProcessors { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operands/Operation.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operators; 2 | 3 | namespace ObservableView.Searching.Operands 4 | { 5 | public abstract class Operation // TODO: Convert this class into an interface 6 | { 7 | // [DataMember(Name = "Operator", IsRequired = true)] 8 | public IOperator Operator { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operands/PropertyOperand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using ObservableView.Searching.Processors; 4 | 5 | namespace ObservableView.Searching.Operands 6 | { 7 | // [DataContract(Name = "PropertyOperand")] 8 | [DebuggerDisplay("PropertyOperand: Name={PropertyInfo.Name}, Type={PropertyInfo.PropertyType.Name}")] 9 | public class PropertyOperand : Operand 10 | { 11 | public PropertyOperand(PropertyInfo propertyInfo, IExpressionProcessor[] expressionProcessors = null) 12 | { 13 | this.PropertyInfo = propertyInfo; 14 | this.ExpressionProcessors = expressionProcessors; 15 | } 16 | 17 | // [DataMember(Name = "PropertyInfo", IsRequired = true)] 18 | public PropertyInfo PropertyInfo { get; set; } 19 | 20 | public override Expression Build(IExpressionBuilder expressionBuilder) 21 | { 22 | Expression propertyExpression = Expression.Property(expressionBuilder.ParameterExpression, this.PropertyInfo); 23 | 24 | // TODO Move ExpressionProcessors to PropertyOperand (not used anywhere else)... 25 | if (this.ExpressionProcessors != null) 26 | { 27 | foreach (var expressionProcessor in this.ExpressionProcessors) 28 | { 29 | propertyExpression = expressionProcessor.Process(propertyExpression); 30 | } 31 | } 32 | 33 | return propertyExpression; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operands/VariableOperand.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ObservableView.Searching.Operands 4 | { 5 | [DebuggerDisplay("VariableOperand: VariableName={VariableName}, Value={Value}")] 6 | public class VariableOperand : ConstantOperand 7 | { 8 | public VariableOperand(string variableName, Type type) 9 | : base(type) 10 | { 11 | this.VariableName = variableName; 12 | } 13 | 14 | public string VariableName { get; private set; } 15 | } 16 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operations/BinaryOperation.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | using ObservableView.Searching.Operators; 3 | 4 | namespace ObservableView.Searching.Operations 5 | { 6 | public class BinaryOperation : Operation 7 | { 8 | public BinaryOperation(BinaryOperator binaryOperator, PropertyOperand leftOperand, IOperand rightOperand) 9 | { 10 | this.Operator = binaryOperator; 11 | this.LeftOperand = leftOperand; 12 | this.RightOperand = rightOperand; 13 | } 14 | 15 | // [DataMember(Name = "LeftOperand", IsRequired = true)] 16 | public IOperand LeftOperand { get; set; } 17 | 18 | // [DataMember(Name = "RightOperand", IsRequired = true)] 19 | public IOperand RightOperand { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operations/GroupOperation.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | using ObservableView.Searching.Operators; 3 | 4 | namespace ObservableView.Searching.Operations 5 | { 6 | // [DataContract(Name = "GroupOperation")] 7 | public class GroupOperation : Operation 8 | { 9 | public GroupOperation(Operation leftOperation, Operation rightOperation, GroupOperator groupOperator) 10 | { 11 | this.LeftOperation = leftOperation; 12 | this.RightOperation = rightOperation; 13 | this.Operator = groupOperator; 14 | } 15 | 16 | // [DataMember(Name = "LeftOperation", IsRequired = true)] 17 | public Operation LeftOperation { get; set; } 18 | 19 | // [DataMember(Name = "RightOperation", IsRequired = true)] 20 | public Operation RightOperation { get; set; } 21 | } 22 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operators/AndOperator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using ObservableView.Searching.Operands; 4 | using ObservableView.Searching.Operations; 5 | 6 | namespace ObservableView.Searching.Operators 7 | { 8 | // [DataContract(Name = "AndOperator")] 9 | [DebuggerDisplay("AndOperator")] 10 | public class AndOperator : GroupOperator 11 | { 12 | public override Expression Build(IExpressionBuilder expressionBuilder, Operation operation) 13 | { 14 | GroupOperation groupOperation = (GroupOperation)operation; 15 | 16 | return Expression.AndAlso(expressionBuilder.Build(groupOperation.LeftOperation), expressionBuilder.Build(groupOperation.RightOperation)); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operators/BinaryOperator.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | using ObservableView.Searching.Operations; 3 | 4 | namespace ObservableView.Searching.Operators 5 | { 6 | public abstract class BinaryOperator : IOperator 7 | { 8 | ////private static readonly IEnumerable derivedTypes; // TODO: MOVE TO BASE 9 | 10 | static BinaryOperator() 11 | { 12 | // Initialize all binary operators in dictionary 13 | ////derivedTypes = FindDerivedTypes().ToList(); 14 | } 15 | 16 | public Expression Build(IExpressionBuilder expressionBuilder, Operation operation) 17 | { 18 | var binaryOperation = (BinaryOperation)operation; 19 | 20 | return this.Build(expressionBuilder, binaryOperation); 21 | } 22 | 23 | public abstract Expression Build(IExpressionBuilder expressionBuilder, BinaryOperation operation); 24 | 25 | ////public static IEnumerable FindDerivedTypes() 26 | ////{ 27 | //// var baseType = typeof(T); 28 | //// var typeInfo = baseType.GetTypeInfo(); 29 | 30 | //// return typeInfo.Assembly.ExportedTypes.Where(t => 31 | //// t.GetTypeInfo().IsClass && 32 | //// t.GetTypeInfo().IsAbstract == false && 33 | //// t.GetTypeInfo().IsSubclassOf(baseType)); 34 | ////} 35 | 36 | public static EqualOperator Equal 37 | { 38 | get 39 | { 40 | return new EqualOperator(); 41 | } 42 | } 43 | 44 | public static ContainsOperator Contains 45 | { 46 | get 47 | { 48 | return new ContainsOperator(); 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operators/ContainsOperator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using ObservableView.Searching.Operations; 3 | 4 | namespace ObservableView.Searching.Operators 5 | { 6 | [DebuggerDisplay("ContainsOperator")] 7 | public class ContainsOperator : BinaryOperator 8 | { 9 | private readonly StringComparison stringComparison; 10 | 11 | public ContainsOperator(StringComparison stringComparison = StringComparison.OrdinalIgnoreCase) 12 | { 13 | this.stringComparison = stringComparison; 14 | } 15 | 16 | public override Expression Build(IExpressionBuilder expressionBuilder, BinaryOperation binaryOperation) 17 | { 18 | var leftExpression = expressionBuilder.Build(binaryOperation.LeftOperand); 19 | 20 | Expression leftNotNullExpression = null; 21 | if (leftExpression.Type.GetTypeInfo().IsValueType == false) 22 | { 23 | var nullExpression = Expression.Constant(null, leftExpression.Type); 24 | leftNotNullExpression = Expression.NotEqual(leftExpression, nullExpression); 25 | } 26 | 27 | if (leftExpression.Type != typeof(string)) 28 | { 29 | leftExpression = leftExpression.ToStringExpression(); // TODO : Move to BinaryStringOperator 30 | } 31 | 32 | var rightExpression = expressionBuilder.Build(binaryOperation.RightOperand); 33 | 34 | Expression rightNotNullExpression = null; 35 | if (rightExpression.Type.GetTypeInfo().IsValueType == false) 36 | { 37 | var nullExpression = Expression.Constant(null, rightExpression.Type); 38 | rightNotNullExpression = Expression.NotEqual(rightExpression, nullExpression); 39 | } 40 | 41 | if (rightExpression.Type != typeof(string)) 42 | { 43 | rightExpression = rightExpression.ToStringExpression(); // TODO : Move to BinaryStringOperator 44 | } 45 | 46 | MethodInfo containsExtensionMethodInfo = typeof(StringExtensions).GetRuntimeMethod(nameof(StringExtensions.Contains), new[] { typeof(string), typeof(string), typeof(StringComparison) }); 47 | //MethodInfo containsMethodInfo = ReflectionHelper.GetMethod((source, argument) => source.Contains(argument, this.stringComparison)); 48 | var stringComparisonExpression = Expression.Constant(this.stringComparison); 49 | 50 | //#if IOS 51 | // Expression containsExpression = Expression.Call( 52 | // leftExpression, 53 | // containsExtensionMethodInfo, 54 | // rightExpression, 55 | // stringComparisonExpression); 56 | 57 | //#else 58 | Expression containsExpression = Expression.Call( 59 | null, 60 | containsExtensionMethodInfo, 61 | leftExpression, 62 | rightExpression, 63 | stringComparisonExpression); 64 | //#endif 65 | 66 | if (leftNotNullExpression != null) 67 | { 68 | containsExpression = Expression.AndAlso(leftNotNullExpression, containsExpression); 69 | } 70 | 71 | if (rightNotNullExpression != null) 72 | { 73 | containsExpression = Expression.AndAlso(rightNotNullExpression, containsExpression); 74 | } 75 | 76 | return containsExpression; 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operators/EqualOperator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using ObservableView.Searching.Operations; 4 | 5 | namespace ObservableView.Searching.Operators 6 | { 7 | // [DataContract(Name = "EqualOperator")] 8 | [DebuggerDisplay("EqualOperator")] 9 | public class EqualOperator : BinaryOperator 10 | { 11 | public override Expression Build(IExpressionBuilder expressionBuilder, BinaryOperation binaryOperation) 12 | { 13 | Expression leftExpression = expressionBuilder.Build(binaryOperation.LeftOperand); 14 | Expression rightExpression = expressionBuilder.Build(binaryOperation.RightOperand); 15 | 16 | Expression equalExpression = Expression.Equal(leftExpression, rightExpression); 17 | return equalExpression; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operators/GroupOperator.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | 3 | namespace ObservableView.Searching.Operators 4 | { 5 | // [DataContract(Name = "GroupOperator")] 6 | public abstract class GroupOperator : IOperator 7 | { 8 | public abstract Expression Build(IExpressionBuilder expressionBuilder, Operation operation); 9 | 10 | public static AndOperator And 11 | { 12 | get 13 | { 14 | return new AndOperator(); 15 | } 16 | } 17 | 18 | public static OrOperator Or 19 | { 20 | get 21 | { 22 | return new OrOperator(); 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operators/IOperator.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | 3 | namespace ObservableView.Searching.Operators 4 | { 5 | public interface IOperator 6 | { 7 | /// 8 | /// Builds the expression of the parameter. 9 | /// 10 | /// The expression builder. 11 | /// The operation. 12 | /// The expression. 13 | Expression Build(IExpressionBuilder expressionBuilder, Operation operation); 14 | } 15 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Operators/OrOperator.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using ObservableView.Searching.Operands; 4 | using ObservableView.Searching.Operations; 5 | 6 | namespace ObservableView.Searching.Operators 7 | { 8 | // [DataContract(Name = "OrOperator")] 9 | [DebuggerDisplay("OrOperator")] 10 | public class OrOperator : GroupOperator 11 | { 12 | public override Expression Build(IExpressionBuilder expressionBuilder, Operation operation) 13 | { 14 | GroupOperation groupOperation = (GroupOperation)operation; 15 | 16 | return Expression.OrElse(expressionBuilder.Build(groupOperation.LeftOperation), expressionBuilder.Build(groupOperation.RightOperation)); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Processors/ExpressionProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Searching.Processors 2 | { 3 | // [DataContract(Name = "ExpressionProcessor")] 4 | public abstract class ExpressionProcessor : IExpressionProcessor 5 | { 6 | public abstract Expression Process(Expression expression); 7 | 8 | public static ToLowerExpressionProcessor ToLower 9 | { 10 | get 11 | { 12 | return new ToLowerExpressionProcessor(); 13 | } 14 | } 15 | 16 | public static ToUpperExpressionProcessor ToUpper 17 | { 18 | get 19 | { 20 | return new ToUpperExpressionProcessor(); 21 | } 22 | } 23 | 24 | public static TrimExpressionProcessor Trim 25 | { 26 | get 27 | { 28 | return new TrimExpressionProcessor(); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Processors/IExpressionProcessor.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Searching.Processors 2 | { 3 | public interface IExpressionProcessor 4 | { 5 | Expression Process(Expression expression); 6 | } 7 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Processors/ToLowerExpressionProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ObservableView.Searching.Processors 4 | { 5 | [DebuggerDisplay("ToLowerExpressionProcessor")] 6 | public class ToLowerExpressionProcessor : ExpressionProcessor 7 | { 8 | public override Expression Process(Expression expression) 9 | { 10 | Expression toLowerExpression = expression.ToLower(); 11 | 12 | return toLowerExpression; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Processors/ToUpperExpressionProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ObservableView.Searching.Processors 4 | { 5 | [DebuggerDisplay("ToUpperExpressionProcessor")] 6 | public class ToUpperExpressionProcessor : ExpressionProcessor 7 | { 8 | public override Expression Process(Expression expression) 9 | { 10 | Expression toLowerExpression = expression.ToUpper(); 11 | 12 | return toLowerExpression; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /ObservableView/Searching/Processors/TrimExpressionProcessor.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ObservableView.Searching.Processors 4 | { 5 | [DebuggerDisplay("TrimExpressionProcessor")] 6 | public class TrimExpressionProcessor : ExpressionProcessor 7 | { 8 | public override Expression Process(Expression expression) 9 | { 10 | Expression trimExpression = expression.Trim(); 11 | 12 | return trimExpression; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /ObservableView/Searching/SearchSpecification.cs: -------------------------------------------------------------------------------- 1 | using ObservableView.Searching.Operands; 2 | using ObservableView.Searching.Operations; 3 | using ObservableView.Searching.Operators; 4 | 5 | using ObservableView.Searching.Processors; 6 | 7 | namespace ObservableView.Searching 8 | { 9 | public class SearchSpecification : ISearchSpecification 10 | { 11 | private const string DefaultSearchTextVariableName = "searchText"; 12 | 13 | public event EventHandler SearchSpecificationAdded; 14 | 15 | public event EventHandler SearchSpecificationsCleared; 16 | 17 | public Operation BaseOperation { get; private set; } 18 | 19 | private static void EnsureOperator(Type propertyType, ref BinaryOperator @operator) 20 | { 21 | if (@operator == null) 22 | { 23 | // TODO: BinaryOperator @operator = null then take default depending on property type 24 | if (propertyType == typeof(string)) 25 | { 26 | @operator = BinaryOperator.Contains; 27 | } 28 | else if (propertyType == typeof(int)) 29 | { 30 | @operator = BinaryOperator.Equal; 31 | } 32 | } 33 | } 34 | 35 | public ISearchSpecification Add(Expression> propertyExpression, BinaryOperator @operator = null) 36 | { 37 | return this.Add(propertyExpression, null, @operator); 38 | } 39 | 40 | public ISearchSpecification Add(Expression> propertyExpression, IExpressionProcessor[] expressionProcessors, BinaryOperator @operator = null) 41 | { 42 | if (propertyExpression == null) 43 | { 44 | throw new ArgumentNullException("propertyExpression"); 45 | } 46 | 47 | var propertyInfo = ReflectionHelper.GetProperty(propertyExpression); 48 | 49 | EnsureOperator(propertyInfo.PropertyType, ref @operator); 50 | 51 | if (this.BaseOperation == null) 52 | { 53 | this.BaseOperation = new BinaryOperation(@operator, new PropertyOperand(propertyInfo, expressionProcessors), new VariableOperand(DefaultSearchTextVariableName, propertyInfo.PropertyType)); 54 | 55 | this.OnSearchSpecificationAdded(); 56 | } 57 | else 58 | { 59 | return this.Or(propertyExpression, expressionProcessors, @operator); 60 | } 61 | 62 | return this; 63 | } 64 | 65 | public ISearchSpecification And(Expression> propertyExpression, BinaryOperator @operator = null) 66 | { 67 | return this.And(propertyExpression, null, @operator); 68 | } 69 | 70 | public ISearchSpecification And(Expression> propertyExpression, IExpressionProcessor[] expressionProcessors, BinaryOperator @operator = null) 71 | { 72 | return this.CreateNestedOperation(propertyExpression, GroupOperator.And, @operator, expressionProcessors); 73 | } 74 | 75 | public ISearchSpecification Or(Expression> propertyExpression, BinaryOperator @operator = null) 76 | { 77 | return this.Or(propertyExpression, null, @operator); 78 | } 79 | 80 | public ISearchSpecification Or(Expression> propertyExpression, IExpressionProcessor[] expressionProcessors, BinaryOperator @operator = null) 81 | { 82 | return this.CreateNestedOperation(propertyExpression, GroupOperator.Or, @operator, expressionProcessors); 83 | } 84 | 85 | private ISearchSpecification CreateNestedOperation(Expression> propertyExpression, GroupOperator groupOperator, BinaryOperator @operator = null, IExpressionProcessor[] expressionProcessors = null) 86 | { 87 | if (this.BaseOperation == null) 88 | { 89 | throw new InvalidOperationException("Call Add beforehand."); 90 | } 91 | 92 | if (propertyExpression == null) 93 | { 94 | throw new ArgumentNullException("propertyExpression"); 95 | } 96 | 97 | var propertyInfo = ReflectionHelper.GetProperty(propertyExpression); 98 | 99 | EnsureOperator(propertyInfo.PropertyType, ref @operator); 100 | 101 | var nestedBinaryOperation = new BinaryOperation(@operator, new PropertyOperand(propertyInfo, expressionProcessors), new VariableOperand(DefaultSearchTextVariableName, propertyInfo.PropertyType)); 102 | 103 | this.BaseOperation = new GroupOperation(this.BaseOperation, nestedBinaryOperation, groupOperator); 104 | 105 | this.OnSearchSpecificationAdded(); 106 | return this; 107 | } 108 | 109 | private void OnSearchSpecificationAdded() 110 | { 111 | var handler = this.SearchSpecificationAdded; 112 | if (handler != null) 113 | { 114 | handler(this, EventArgs.Empty); 115 | } 116 | } 117 | 118 | public void ReplaceSearchTextVariables(TX value) 119 | { 120 | this.ReplaceVariables(DefaultSearchTextVariableName, value); 121 | } 122 | 123 | private void ReplaceVariables(string variableName, TX value) 124 | { 125 | var variableOperands = this.BaseOperation.Flatten().OfType().Where(v => v.VariableName == variableName); 126 | 127 | foreach (var variableOperand in variableOperands) 128 | { 129 | variableOperand.Value = value; 130 | } 131 | } 132 | 133 | public void Clear() 134 | { 135 | this.BaseOperation = null; 136 | this.OnSearchSpecificationsCleared(); 137 | } 138 | 139 | private void OnSearchSpecificationsCleared() 140 | { 141 | var handler = this.SearchSpecificationsCleared; 142 | if (handler != null) 143 | { 144 | handler(this, EventArgs.Empty); 145 | } 146 | } 147 | 148 | public bool Any() 149 | { 150 | return this.BaseOperation != null; 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /ObservableView/Searching/SearchableAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Searching 2 | { 3 | /// 4 | /// Class SearchableAttribute. 5 | /// This class is used as a marker annotation to select properties which are included in the search expression. 6 | /// 7 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true)] 8 | public class SearchableAttribute : Attribute 9 | { 10 | } 11 | } -------------------------------------------------------------------------------- /ObservableView/Sorting/NaturalSortComparable.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Sorting 2 | { 3 | /// 4 | /// Class NaturalSortComparable. 5 | /// Can be used to sort strings with 'natural sort order'. 6 | /// 7 | /// The comparison algorithm is based on logic which was published here: 8 | /// https://psycodedeveloper.wordpress.com/2013/04/11/numeric-sort-file-system-names-in-c-like-windows-explorer/ 9 | /// 10 | public class NaturalSortComparable : IComparable, 11 | IComparable 12 | { 13 | private readonly string value; 14 | 15 | public NaturalSortComparable(string value) 16 | { 17 | this.value = value; 18 | } 19 | 20 | public int CompareTo(object otherValueObj) 21 | { 22 | return this.CompareTo(otherValueObj as NaturalSortComparable); 23 | } 24 | 25 | public int CompareTo(NaturalSortComparable other) 26 | { 27 | if (this.value == null && other == null) 28 | { 29 | return 0; 30 | } 31 | 32 | if (this.value == null) 33 | { 34 | return -1; 35 | } 36 | 37 | if (other == null) 38 | { 39 | return 1; 40 | } 41 | 42 | var otherValue = other; 43 | 44 | int xIndex = 0; 45 | int yIndex = 0; 46 | 47 | while (xIndex < this.value.Length) 48 | { 49 | if (yIndex >= otherValue.Length) 50 | { 51 | return 1; 52 | } 53 | 54 | if (char.IsDigit(this.value[xIndex])) 55 | { 56 | if (!char.IsDigit(otherValue[yIndex])) 57 | { 58 | return -1; 59 | } 60 | 61 | // Compare the numbers 62 | var xText = new List(); 63 | var yText = new List(); 64 | 65 | for (int i = xIndex; i < this.value.Length; i++) 66 | { 67 | var xChar = this.value[i]; 68 | 69 | if (char.IsDigit(xChar)) 70 | { 71 | xText.Add(xChar); 72 | } 73 | else 74 | { 75 | break; 76 | } 77 | } 78 | 79 | for (int j = yIndex; j < otherValue.Length; j++) 80 | { 81 | var yChar = otherValue[j]; 82 | if (char.IsDigit(yChar)) 83 | { 84 | yText.Add(yChar); 85 | } 86 | else 87 | { 88 | break; 89 | } 90 | } 91 | 92 | var xValue = Convert.ToDecimal(new string(xText.ToArray())); 93 | var yValue = Convert.ToDecimal(new string(yText.ToArray())); 94 | 95 | if (xValue < yValue) 96 | { 97 | return -1; 98 | } 99 | else if (xValue > yValue) 100 | { 101 | return 1; 102 | } 103 | 104 | // Skip 105 | xIndex += xText.Count; 106 | yIndex += yText.Count; 107 | } 108 | else if (char.IsDigit(otherValue[yIndex])) 109 | return 1; 110 | else 111 | { 112 | int difference = char.ToUpperInvariant(this.value[xIndex]).CompareTo(char.ToUpperInvariant(otherValue[yIndex])); 113 | if (difference > 0) 114 | { 115 | return 1; 116 | } 117 | else if (difference < 0) 118 | { 119 | return -1; 120 | } 121 | 122 | xIndex++; 123 | yIndex++; 124 | } 125 | } 126 | 127 | if (yIndex < otherValue.Length) 128 | { 129 | return -1; 130 | } 131 | 132 | return 0; 133 | } 134 | 135 | private char this[int index] 136 | { 137 | get 138 | { 139 | return this.value[index]; 140 | } 141 | } 142 | 143 | private int Length 144 | { 145 | get 146 | { 147 | return this.value == null ? 0 : this.value.Length; 148 | } 149 | } 150 | 151 | public override string ToString() 152 | { 153 | return this.value ?? string.Empty; 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /ObservableView/Sorting/OrderDirection.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Sorting 2 | { 3 | public enum OrderDirection 4 | { 5 | Ascending, 6 | Descending 7 | } 8 | } -------------------------------------------------------------------------------- /ObservableView/Sorting/OrderSpecification.cs: -------------------------------------------------------------------------------- 1 | namespace ObservableView.Sorting 2 | { 3 | internal class OrderSpecification // TODO GATH: Could be a struct? 4 | { 5 | public OrderSpecification(Expression> keySelector, OrderDirection orderDirection) 6 | { 7 | this.KeySelector = keySelector.Compile(); 8 | this.PropertyName = ReflectionHelper.GetProperty(keySelector).Name; 9 | this.OrderDirection = orderDirection; 10 | } 11 | 12 | public string PropertyName { get; private set; } 13 | 14 | public Func KeySelector { get; private set; } 15 | 16 | public OrderDirection OrderDirection { get; private set; } 17 | } 18 | } -------------------------------------------------------------------------------- /ObservableView/Utils/TaskDelayer.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ObservableView.Utils 6 | { 7 | public class TaskDelayer 8 | { 9 | private CancellationTokenSource throttleCts = new CancellationTokenSource(); 10 | 11 | /// 12 | /// Runs the given in a background thread with a sliding . 13 | /// 14 | public Task RunWithDelay(TimeSpan delay, Action action) 15 | { 16 | return this.RunWithDelay(delay, () => 17 | { 18 | action(); 19 | return Task.CompletedTask; 20 | }); 21 | } 22 | 23 | /// 24 | /// Runs the given in a background thread with a sliding . 25 | /// 26 | /// 27 | /// Sliding delay. 28 | /// The task to be executed after delay. 29 | /// The default value to be returned in case of a cancellation 30 | /// 31 | public Task RunWithDelay(TimeSpan delay, Func> task, Func defaultValue) 32 | { 33 | var tcs = new TaskCompletionSource(); 34 | 35 | Task.Factory.StartNew(async () => 36 | { 37 | try 38 | { 39 | Interlocked.Exchange(ref this.throttleCts, new CancellationTokenSource()).Cancel(); 40 | 41 | Debug.WriteLine($"RunWithDelay {delay} starts now"); 42 | await Task.Delay(delay, this.throttleCts.Token) 43 | .ContinueWith(async ct => 44 | { 45 | try 46 | { 47 | var result = await task().ConfigureAwait(false); 48 | tcs.TrySetResult(result); 49 | } 50 | catch (Exception ex) 51 | { 52 | tcs.TrySetException(ex); 53 | } 54 | }, 55 | CancellationToken.None, 56 | TaskContinuationOptions.OnlyOnRanToCompletion, 57 | TaskScheduler.Default); 58 | } 59 | catch (Exception ex) 60 | { 61 | Debug.WriteLine($"RunWithDelay failed with exception: {ex}"); 62 | tcs.TrySetResult(defaultValue()); 63 | } 64 | }).ConfigureAwait(false); 65 | 66 | return tcs.Task; 67 | } 68 | 69 | /// 70 | /// Runs the given in a background thread with a sliding . 71 | /// 72 | /// Sliding delay. 73 | /// The task to be executed after delay. 74 | public async Task RunWithDelay(TimeSpan delay, Func task) 75 | { 76 | try 77 | { 78 | Interlocked.Exchange(ref this.throttleCts, new CancellationTokenSource()).Cancel(); 79 | 80 | Debug.WriteLine($"RunWithDelay {delay} starts now"); 81 | await Task.Delay(delay, this.throttleCts.Token) 82 | .ContinueWith(async ct => await task().ConfigureAwait(false), 83 | CancellationToken.None, 84 | TaskContinuationOptions.OnlyOnRanToCompletion, 85 | TaskScheduler.Default); 86 | } 87 | catch (TaskCanceledException) 88 | { 89 | // Ignore TaskCanceledException 90 | } 91 | catch (Exception ex) 92 | { 93 | Debug.WriteLine($"RunWithDelay failed with exception: {ex}"); 94 | throw; 95 | } 96 | } 97 | 98 | public void Cancel() 99 | { 100 | this.throttleCts.Cancel(); 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # ObservableView 2 | [![Version](https://img.shields.io/nuget/v/ObservableView.svg)](https://www.nuget.org/packages/Paging.NET) [![Downloads](https://img.shields.io/nuget/dt/ObservableView.svg)](https://www.nuget.org/packages/Paging.NET) [![Buy Me a Coffee](https://img.shields.io/badge/support-buy%20me%20a%20coffee-FFDD00)](https://buymeacoffee.com/thomasgalliker) 3 | 4 | ObservableView is a simple wrapper for collections which provides an easy-to-use API for searching, filtering, sorting and grouping of collections. This project enhances the well-known ObservableCollection of the .Net Framework with addition, commonly-used features. The goal is to have a Swiss army knife of a collection utility which provides an easy-to-use but very powerful API while preserving maximum platform compatibility. 5 | 6 | ### Download and Install ObservableView 7 | This library is available on NuGet: https://www.nuget.org/packages/ObservableView/ 8 | Use the following command to install ObservableView using NuGet package manager console: 9 | 10 | PM> Install-Package ObservableView 11 | 12 | You can use this library in any .NET project which is compatible to .NET Standard 2.0 and higher. 13 | 14 | ### API Usage 15 | #### Basic MVVM data binding with List Views 16 | The usage of `ObservableView` is not much different from `ObservableCollection`: 17 | 1) Create a public `ObservableView` property in your ViewModel. 18 | ```C# 19 | public ObservableView MallList { get; } 20 | ``` 21 | 22 | 2) Fill the `ObservableView.Source` with item view models. 23 | ```C# 24 | public MallListViewModel(IMallService mallService) 25 | { 26 | var allMalls = mallService.GetAllMalls(); 27 | this.MallList = new ObservableView(allMalls); 28 | } 29 | ``` 30 | 31 | 3) Create a View with a ListView (or any other collection control) and bind the items source to `ObservableView.View`. 32 | ```C# 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | ``` 57 | 58 | As you can observe in the example above, the XAML view binds to `MallList.View`. This is important in order to reflect operation (search, filter, group...) performed on the source collection. 59 | 60 | #### Add, remove, update source collection 61 | If you need to add or remove items of the source collection, you can simply do so by manipulating the MallList.Source property. By doing so, it automatically refreshes all dependent properties (e.g. View). 62 | 63 | #### Search 64 | Two steps are necessary in order to enable the search functionality: 65 | 1) Define search specification(s) for properties of your collection item type ```T```: 66 | - Call ```SearchSpecification.Add``` for searchable properties: 67 | ```C# 68 | this.MallsList.SearchSpecification.Add(x => x.Title, BinaryOperator.Contains); 69 | this.MallsList.SearchSpecification.Add(x => x.Subtitle, BinaryOperator.Contains); 70 | ``` 71 | - Alternative: Annotate searchable properties with ```[Searchable]``` 72 | 2) The search operation can be triggered either from within the ViewModel using ```ObservableView.Search(...)``` method or by binding ```ObservableView.SearchText``` in XAML to an input textbox. 73 | 74 | #### Filtering 75 | 1) Subscribe FilterHandler event: 76 | ```C# 77 | this.MallsList.FilterHandler += this.MallsList_FilterHandler; 78 | ``` 79 | 80 | 2) Specify with each collection item if it is filtered or not: 81 | ```C# 82 | private void MallsList_FilterHandler(object sender, ObservableView.Filtering.FilterEventArgs e) 83 | { 84 | if (e.Item.Title.Contains("Aber")) 85 | { 86 | e.IsAllowed = false; 87 | } 88 | } 89 | ``` 90 | 91 | #### Sorting 92 | There are many ways of how collections can be presented with defined sort orders. Method AddOrderSpecification can be used to set-up sort specifications for properties of type T. 93 | ```C# 94 | this.MallsList.AddOrderSpecification(x => x.Title, OrderDirection.Ascending); 95 | this.MallsList.AddOrderSpecification(x => x.Subtitle, OrderDirection.Descending); 96 | ``` 97 | 98 | In the XAML, we could either bind the ItemsSource property to MallsList.View or we can use the attached dependency property ```ObservableViewExtensions.ObservableView``` to bind MallsList directly to the DataGrid. The latter approach enables you to make use of multi-column sorting using the DataGrid headers. 99 | ```C# 100 | 102 | 103 | 104 | 105 | 106 | 107 | ``` 108 | 109 | TODO: Describe how to use IComparer with custom column sort algorithms. 110 | 111 | #### Grouping 112 | ObservableView allows to specify a grouping algorithm as well as the key by which the collection is grouped: 113 | ```C# 114 | this.MallsList.GroupKeyAlogrithm = new AlphaGroupKeyAlgorithm(); 115 | this.MallsList.GroupKey = mall => mall.Title; 116 | ``` 117 | 118 | ### Performance considerations 119 | Performance is a critical success factor for ObservableView. ObservableView has been tested with ten thousands of data records with good results. If you run into performance bottlenecks caused by ObservableView, do not hesitate to open a new issue. 120 | 121 | ### Contribution 122 | Contributors welcome! If you find a bug or you want to propose a new feature, feel free to do so by opening a new issue on github.com. 123 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/App.xaml: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using MauiSampleApp.Views; 2 | 3 | namespace MauiSampleApp 4 | { 5 | public partial class App : Application 6 | { 7 | public App() 8 | { 9 | this.InitializeComponent(); 10 | 11 | var mainPage = IPlatformApplication.Current.Services.GetRequiredService(); 12 | this.MainPage = new NavigationPage(mainPage); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using MauiSampleApp.Views; 2 | using Microsoft.Extensions.Logging; 3 | using ObservableViewSample.Service; 4 | using ObservableViewSample.ViewModel; 5 | 6 | namespace MauiSampleApp 7 | { 8 | public static class MauiProgram 9 | { 10 | public static MauiApp CreateMauiApp() 11 | { 12 | var builder = MauiApp.CreateBuilder(); 13 | builder 14 | .UseMauiApp() 15 | .ConfigureFonts(fonts => 16 | { 17 | fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); 18 | fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); 19 | }); 20 | 21 | builder.Services.AddLogging(b => 22 | { 23 | b.ClearProviders(); 24 | b.SetMinimumLevel(LogLevel.Trace); 25 | b.AddDebug(); 26 | }); 27 | 28 | builder.Services.AddSingleton(); 29 | builder.Services.AddTransient(); 30 | builder.Services.AddTransient(); 31 | 32 | return builder.Build(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/MauiSampleApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0-android;net8.0-ios 5 | $(TargetFrameworks);net8.0-windows10.0.19041.0 6 | Exe 7 | MauiSampleApp 8 | true 9 | true 10 | enable 11 | disable 12 | false 13 | 14 | 15 | MauiSampleApp 16 | 17 | 18 | com.companyname.mauisampleapp 19 | 20 | 21 | 1.0 22 | 1 23 | 24 | 11.0 25 | 21.0 26 | 10.0.17763.0 27 | 10.0.17763.0 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | 4 | namespace MauiSampleApp 5 | { 6 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] 7 | public class MainActivity : MauiAppCompatActivity 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace MauiSampleApp 5 | { 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) 10 | : base(handle, ownership) 11 | { 12 | } 13 | 14 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #512BD4 4 | #2B0B98 5 | #2B0B98 6 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Windows/App.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Windows/App.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace MauiSampleApp.WinUI 2 | { 3 | public partial class App : MauiWinUIApplication 4 | { 5 | public App() 6 | { 7 | this.InitializeComponent(); 8 | } 9 | 10 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Windows/Package.appxmanifest: -------------------------------------------------------------------------------- 1 |  2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | $placeholder$ 15 | User Name 16 | $placeholder$.png 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/Windows/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | true/PM 12 | PerMonitorV2, PerMonitor 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using Foundation; 2 | 3 | namespace MauiSampleApp 4 | { 5 | [Register("AppDelegate")] 6 | public class AppDelegate : MauiUIApplicationDelegate 7 | { 8 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LSRequiresIPhoneOS 6 | 7 | UIDeviceFamily 8 | 9 | 1 10 | 2 11 | 12 | UIRequiredDeviceCapabilities 13 | 14 | arm64 15 | 16 | UISupportedInterfaceOrientations 17 | 18 | UIInterfaceOrientationPortrait 19 | UIInterfaceOrientationLandscapeLeft 20 | UIInterfaceOrientationLandscapeRight 21 | 22 | UISupportedInterfaceOrientations~ipad 23 | 24 | UIInterfaceOrientationPortrait 25 | UIInterfaceOrientationPortraitUpsideDown 26 | UIInterfaceOrientationLandscapeLeft 27 | UIInterfaceOrientationLandscapeRight 28 | 29 | XSAppIconAssets 30 | Assets.xcassets/appicon.appiconset 31 | 32 | 33 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Platforms/iOS/Program.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace MauiSampleApp 4 | { 5 | public class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | UIApplication.Main(args, null, typeof(AppDelegate)); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "Windows Machine": { 4 | "commandName": "MsixPackage", 5 | "nativeDebugging": false 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/AppIcon/appicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/AppIcon/appiconfg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/Fonts/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasgalliker/ObservableView/ee3f72dbf0384bd052755cb16f524bde95486167/Samples/MauiSampleApp/Resources/Fonts/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/Fonts/OpenSans-Semibold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasgalliker/ObservableView/ee3f72dbf0384bd052755cb16f524bde95486167/Samples/MauiSampleApp/Resources/Fonts/OpenSans-Semibold.ttf -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/Images/dotnet_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thomasgalliker/ObservableView/ee3f72dbf0384bd052755cb16f524bde95486167/Samples/MauiSampleApp/Resources/Images/dotnet_bot.png -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/Raw/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories). Deployment of the asset to your application 3 | is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. 4 | 5 | 6 | 7 | These files will be deployed with you package and will be accessible using Essentials: 8 | 9 | async Task LoadMauiAsset() 10 | { 11 | using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); 12 | using var reader = new StreamReader(stream); 13 | 14 | var contents = reader.ReadToEnd(); 15 | } 16 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/Splash/splash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Resources/Styles/Colors.xaml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 6 | 7 | 8 | 9 | #512BD4 10 | #ac99ea 11 | #242424 12 | #DFD8F7 13 | #9880e5 14 | #2B0B98 15 | 16 | White 17 | Black 18 | #D600AA 19 | #190649 20 | #1f1f1f 21 | 22 | #E1E1E1 23 | #C8C8C8 24 | #ACACAC 25 | #919191 26 | #6E6E6E 27 | #404040 28 | #212121 29 | #141414 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Views/ItemTemplates/MallItemTemplate.xaml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | 13 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Views/ItemTemplates/MallItemTemplate.xaml.cs: -------------------------------------------------------------------------------- 1 | namespace MauiSampleApp.Views.ItemTemplates 2 | { 3 | public partial class MallItemTemplate : ViewCell 4 | { 5 | public MallItemTemplate() 6 | { 7 | this.InitializeComponent(); 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Views/MainPage.xaml: -------------------------------------------------------------------------------- 1 |  2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Samples/MauiSampleApp/Views/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using ObservableViewSample.ViewModel; 2 | 3 | namespace MauiSampleApp.Views 4 | { 5 | public partial class MainPage : ContentPage 6 | { 7 | public MainPage(MainViewModel mainViewModel) 8 | { 9 | this.InitializeComponent(); 10 | this.BindingContext = mainViewModel; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Samples/ObservableViewSample/Model/Mall.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace ObservableViewSample.Model 4 | { 5 | [DebuggerDisplay("Mall: {this.Title}, {this.Subtitle}")] 6 | public class Mall 7 | { 8 | public string Title { get; set; } 9 | 10 | public string Subtitle { get; set; } 11 | 12 | public Mall(string title, string subtitle) 13 | { 14 | this.Title = title; 15 | this.Subtitle = subtitle; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Samples/ObservableViewSample/ObservableViewSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | latestmajor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Samples/ObservableViewSample/Service/IMallManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | using ObservableViewSample.Model; 4 | 5 | namespace ObservableViewSample.Service 6 | { 7 | public interface IMallManager 8 | { 9 | ObservableCollection GetMalls(); 10 | } 11 | } -------------------------------------------------------------------------------- /Samples/ObservableViewSample/ViewModel/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CommunityToolkit.Mvvm.Input; 4 | using ObservableView; 5 | using ObservableView.Grouping; 6 | using ObservableView.Searching.Operators; 7 | using ObservableView.Sorting; 8 | 9 | using ObservableViewSample.Model; 10 | using ObservableViewSample.Service; 11 | using ObservableObject = CommunityToolkit.Mvvm.ComponentModel.ObservableObject; 12 | 13 | namespace ObservableViewSample.ViewModel 14 | { 15 | public class MainViewModel : ObservableObject 16 | { 17 | private RelayCommand addMallCommand; 18 | private RelayCommand deleteMallCommand; 19 | private IAsyncRelayCommand refreshCommand; 20 | private RelayCommand searchBoxClearCommand; 21 | private string newMallTitle; 22 | private string newMallSubtitle; 23 | private int newMallNumberOf = 1; 24 | private bool isRefreshing; 25 | 26 | public MainViewModel(IMallManager mallManager) 27 | { 28 | var listItems = mallManager.GetMalls(); 29 | 30 | this.MallsList = new ObservableView(listItems); 31 | 32 | // Add sort specifications 33 | this.MallsList.AddOrderSpecification(x => x.Title, OrderDirection.Ascending); 34 | this.MallsList.AddOrderSpecification(x => x.Subtitle, OrderDirection.Descending); 35 | 36 | // Add search specifications 37 | this.MallsList.SearchSpecification.Add(x => x.Title, BinaryOperator.Contains); 38 | this.MallsList.SearchSpecification.Add(x => x.Subtitle, BinaryOperator.Contains); 39 | 40 | // Add grouping specifications 41 | this.MallsList.GroupKeyAlgorithm = new AlphaGroupKeyAlgorithm(); 42 | this.MallsList.GroupKey = mall => mall.Title; 43 | 44 | this.MallsList.FilterHandler += this.MallsList_FilterHandler; 45 | } 46 | 47 | private void MallsList_FilterHandler(object sender, ObservableView.Filtering.FilterEventArgs e) 48 | { 49 | if (e.Item.Title.Contains("Aber")) 50 | { 51 | e.IsAllowed = false; 52 | } 53 | } 54 | 55 | public ObservableView MallsList { get; private set; } 56 | 57 | public RelayCommand AddMallCommand 58 | { 59 | get 60 | { 61 | return this.addMallCommand ?? (this.addMallCommand = new RelayCommand( 62 | () => 63 | { 64 | // Add new item to ObservableView 65 | for (int i = 0; i < this.NewMallNumberOf; i++) 66 | { 67 | this.MallsList.Source.Add(new Mall(this.NewMallTitle + (i > 0 ? "_" + i : ""), this.NewMallSubtitle)); 68 | } 69 | 70 | // Reset the text input 71 | this.NewMallTitle = string.Empty; 72 | this.NewMallSubtitle = string.Empty; 73 | })); 74 | } 75 | } 76 | 77 | public RelayCommand DeleteMallCommand 78 | { 79 | get 80 | { 81 | return this.deleteMallCommand ?? (this.deleteMallCommand = new RelayCommand( 82 | (mall) => 83 | { 84 | // Remove new item to ObservableView 85 | this.MallsList.Source.Remove(mall); 86 | 87 | // Reset the text input 88 | this.NewMallTitle = string.Empty; 89 | this.NewMallSubtitle = string.Empty; 90 | })); 91 | } 92 | } 93 | 94 | public IAsyncRelayCommand RefreshCommand 95 | { 96 | get => this.refreshCommand ??= new AsyncRelayCommand(this.RefreshAsync); 97 | } 98 | 99 | public async Task RefreshAsync() 100 | { 101 | this.IsRefreshing = true; 102 | 103 | try 104 | { 105 | await Task.Delay(1000); 106 | this.MallsList.Refresh(); 107 | } 108 | finally 109 | { 110 | this.IsRefreshing = false; 111 | } 112 | } 113 | 114 | public bool IsRefreshing 115 | { 116 | get => this.isRefreshing; 117 | set => this.SetProperty(ref this.isRefreshing, value); 118 | } 119 | 120 | public RelayCommand SearchBoxClearCommand => this.searchBoxClearCommand ??= new RelayCommand(this.MallsList.ClearSearch); 121 | 122 | public string NewMallTitle 123 | { 124 | get => this.newMallTitle; 125 | set 126 | { 127 | if (this.SetProperty(ref this.newMallTitle, value)) 128 | { 129 | this.OnPropertyChanged(nameof(this.NewMallTitle)); 130 | this.OnPropertyChanged(nameof(this.IsAddMallButtonEnabled)); 131 | } 132 | } 133 | } 134 | 135 | public string NewMallSubtitle 136 | { 137 | get => this.newMallSubtitle; 138 | set 139 | { 140 | this.newMallSubtitle = value; 141 | this.OnPropertyChanged(nameof(this.NewMallSubtitle)); 142 | this.OnPropertyChanged(nameof(this.IsAddMallButtonEnabled)); 143 | } 144 | } 145 | 146 | public int NewMallNumberOf 147 | { 148 | get => this.newMallNumberOf; 149 | set 150 | { 151 | this.newMallNumberOf = Math.Abs(value); 152 | this.OnPropertyChanged(nameof(this.NewMallNumberOf)); 153 | } 154 | } 155 | 156 | public bool IsAddMallButtonEnabled 157 | { 158 | get => !string.IsNullOrEmpty(this.NewMallTitle) && !string.IsNullOrEmpty(this.NewMallSubtitle); 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /Samples/Settings.XamlStyler: -------------------------------------------------------------------------------- 1 | { 2 | "AttributesTolerance": 1, 3 | "KeepFirstAttributeOnSameLine": false, 4 | "MaxAttributeCharactersPerLine": 0, 5 | "MaxAttributesPerLine": 1, 6 | "NewlineExemptionElements": "RadialGradientBrush, GradientStop, LinearGradientBrush, ScaleTransform, SkewTransform, RotateTransform, TranslateTransform, Trigger, Condition, Setter", 7 | "SeparateByGroups": false, 8 | "AttributeIndentation": 0, 9 | "AttributeIndentationStyle": 1, 10 | "RemoveDesignTimeReferences": false, 11 | "EnableAttributeReordering": true, 12 | "AttributeOrderingRuleGroups": [ 13 | "xmlns, xmlns:x", 14 | "xmlns:*", 15 | "x:Class", 16 | "x:Key, Key, x:Name, Name, x:Uid, Uid, Title", 17 | "Grid.Row, Grid.RowSpan, Grid.Column, Grid.ColumnSpan, Canvas.Left, Canvas.Top, Canvas.Right, Canvas.Bottom", 18 | "Content", 19 | "Width, Height, MinWidth, MinHeight, MaxWidth, MaxHeight", 20 | "Margin, Padding, HorizontalAlignment, VerticalAlignment, HorizontalContentAlignment, VerticalContentAlignment, Panel.ZIndex", 21 | "Source", 22 | "*:*, *", 23 | "PageSource, PageIndex, Offset, Color, TargetName, Property, Value, StartPoint, EndPoint", 24 | "mc:Ignorable, d:IsDataSource, d:LayoutOverrides, d:IsStaticText", 25 | "Storyboard.*, From, To, Duration", 26 | "Horizontal*, Vertical*", 27 | "Start*, End*" 28 | ], 29 | "FirstLineAttributes": "", 30 | "OrderAttributesByName": true, 31 | "PutEndingBracketOnNewLine": false, 32 | "RemoveEndingTagOfEmptyElement": true, 33 | "SpaceBeforeClosingSlash": true, 34 | "RootElementLineBreakRule": 0, 35 | "ReorderVSM": 2, 36 | "ReorderGridChildren": false, 37 | "ReorderCanvasChildren": false, 38 | "ReorderSetters": 0, 39 | "FormatMarkupExtension": true, 40 | "NoNewLineMarkupExtensions": "x:Bind, Binding", 41 | "ThicknessSeparator": 2, 42 | "ThicknessAttributes": "Margin, Padding, BorderThickness, ThumbnailClipMargin", 43 | "FormatOnSave": true, 44 | "CommentPadding": 2 45 | } -------------------------------------------------------------------------------- /Samples/WpfSampleApp/App.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | -------------------------------------------------------------------------------- /Samples/WpfSampleApp/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | 3 | namespace WpfSampleApp 4 | { 5 | public partial class App : Application 6 | { 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /Samples/WpfSampleApp/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 | -------------------------------------------------------------------------------- /Samples/WpfSampleApp/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 17 | 18 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /Samples/WpfSampleApp/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows; 2 | using ObservableViewSample.Service; 3 | using ObservableViewSample.ViewModel; 4 | 5 | namespace WpfSampleApp 6 | { 7 | public partial class MainWindow : Window 8 | { 9 | public MainWindow() 10 | { 11 | this.InitializeComponent(); 12 | this.DataContext = new MainViewModel(new MallManager()); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Samples/WpfSampleApp/ViewModels/FilterItemViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | using ObservableView.Searching.Operators; 3 | 4 | namespace WpfSampleApp.ViewModels 5 | { 6 | public class FilterItemViewModel : ObservableObject 7 | { 8 | private BinaryOperator selectedOperator; 9 | 10 | public FilterItemViewModel() 11 | { 12 | 13 | } 14 | 15 | public BinaryOperator SelectedOperator 16 | { 17 | get 18 | { 19 | return this.selectedOperator; 20 | } 21 | 22 | set 23 | { 24 | if (value != this.selectedOperator) 25 | { 26 | this.selectedOperator = value; 27 | this.OnPropertyChanged(nameof(this.SelectedOperator)); 28 | //this.OnFilterCriteriaChanged(); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Samples/WpfSampleApp/ViewModels/FilterViewModel.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Mvvm.ComponentModel; 2 | 3 | namespace WpfSampleApp.ViewModels 4 | { 5 | public class FilterViewModel : ObservableObject 6 | { 7 | public FilterViewModel() 8 | { 9 | this.FilterItems = new List 10 | { 11 | new FilterItemViewModel(), 12 | new FilterItemViewModel() 13 | }; 14 | } 15 | 16 | public IEnumerable FilterItems { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Samples/WpfSampleApp/Views/FilterControl.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 18 | 19 | 30 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Samples/WpfSampleApp/Views/FilterControl.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using WpfSampleApp.ViewModels; 3 | 4 | namespace WpfSampleApp.Views 5 | { 6 | public partial class FilterControl : UserControl 7 | { 8 | public FilterControl() 9 | { 10 | this.InitializeComponent(); 11 | this.DataContext = new FilterViewModel(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Samples/WpfSampleApp/Views/FilterItemView.xaml: -------------------------------------------------------------------------------- 1 |  10 | 11 | 12 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 47 | 58 | 67 | 68 | 69 | 70 | 79 | 80 | 81 | 82 | 85 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 100 | 101 | 102 | 103 | 104 |