├── .editorconfig ├── .gitattributes ├── .github ├── renovate.json └── workflows │ ├── ci-build.yml │ ├── lock.yml │ └── release.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── images └── logo.png ├── src ├── .editorconfig ├── ReactiveMarbles.Mvvm.Benchmarks │ ├── .editorconfig │ ├── Memory │ │ ├── DummyReactiveObject.cs │ │ ├── DummyRxObject.cs │ │ ├── ReactiveObjectMemoryBenchmark.cs │ │ └── RxObjectMemoryBenchmark.cs │ ├── Performance │ │ ├── AsValuePerformanceBenchmark.cs │ │ └── ToPropertyPerformanceBenchmark.cs │ ├── Program.cs │ ├── README.MD │ ├── ReactiveMarbles.Mvvm.Benchmarks.csproj │ ├── directory.build.props │ └── directory.build.targets ├── ReactiveMarbles.Mvvm.Tests │ ├── AsLazyValueExtensionsTests.cs │ ├── AsLazyValueTestObject.cs │ ├── AsValueExtensionsTests.cs │ ├── AsValueTestData.cs │ ├── AsValueTestObject.cs │ ├── Properties │ │ ├── Resources.Designer.cs │ │ └── Resources.resx │ ├── ReactiveMarbles.Mvvm.Tests.csproj │ ├── ReactiveProperty │ │ ├── Mocks │ │ │ └── ReactivePropertyVM.cs │ │ ├── ReactivePropertyTest.cs │ │ └── TestEnum.cs │ ├── RxObjectTestFixture.cs │ ├── RxObjectTests.cs │ ├── RxRecordTestFixture.cs │ ├── RxRecordTests.cs │ ├── ScheduledSubjectTests.cs │ ├── TestObject.cs │ └── TestSequencer.cs ├── ReactiveMarbles.Mvvm.sln ├── ReactiveMarbles.Mvvm │ ├── AsValueExtensions.cs │ ├── CoreRegistration.cs │ ├── CoreRegistrationBuilder.cs │ ├── DebugExceptionHandler.cs │ ├── ICoreRegistration.cs │ ├── ILoggable.cs │ ├── IRxObject.cs │ ├── IRxPropertyEventArgs.cs │ ├── IRxPropertyEventArgsExtensions.cs │ ├── IStateHandler.cs │ ├── IStateHost.cs │ ├── IThrownExceptions.cs │ ├── IValueBinder.cs │ ├── IsExternalInit.cs │ ├── LazyValueBinder.cs │ ├── LocatorExtensions.cs │ ├── Notifications.cs │ ├── ProxyScheduledSubject.cs │ ├── ReactiveMarbles.Mvvm.csproj │ ├── ReactiveProperty │ │ ├── IReactiveProperty.cs │ │ ├── ReactiveProperty.cs │ │ ├── ReactivePropertyMixins.cs │ │ ├── SingletonDataErrorsChangedEventArgs.cs │ │ └── SingletonPropertyChangedEventArgs.cs │ ├── RxDisposableObject.cs │ ├── RxObject.cs │ ├── RxPropertyChangedEventArgs.cs │ ├── RxPropertyChangingEventArgs.cs │ ├── RxRecord.cs │ ├── UnhandledErrorException.cs │ └── ValueBinder.cs ├── directory.build.props ├── directory.build.targets ├── directory.packages.props ├── nuget.config └── stylecop.json └── version.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Default settings: 5 | # A newline ending every file 6 | # Use 4 spaces as indentation 7 | [*] 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [project.json] 13 | indent_size = 2 14 | 15 | # C# files 16 | [*.cs] 17 | 18 | # SA1101: Prefix local calls with this 19 | dotnet_diagnostic.SA1101.severity = none 20 | 21 | # SA1309: Field names should not begin with underscore 22 | dotnet_diagnostic.SA1309.severity = none 23 | 24 | # SX1309: Field names should begin with underscore 25 | dotnet_diagnostic.SX1309.severity = error 26 | 27 | # SX1309S: static fields should start with _ 28 | dotnet_diagnostic.SX1309S.severity = error 29 | 30 | # Xml project files 31 | [*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] 32 | indent_size = 2 33 | csharp_indent_labels = one_less_than_current 34 | csharp_using_directive_placement = outside_namespace:silent 35 | csharp_prefer_simple_using_statement = true:error 36 | csharp_prefer_braces = true:error 37 | csharp_style_namespace_declarations = block_scoped:suggestion 38 | csharp_style_prefer_method_group_conversion = true:suggestion 39 | csharp_style_prefer_top_level_statements = true:suggestion 40 | csharp_style_expression_bodied_methods = false:warning 41 | csharp_style_expression_bodied_constructors = false:warning 42 | csharp_style_expression_bodied_operators = false:warning 43 | csharp_style_expression_bodied_properties = true:warning 44 | csharp_style_expression_bodied_indexers = true:warning 45 | csharp_style_expression_bodied_accessors = true:warning 46 | csharp_style_expression_bodied_lambdas = true:warning 47 | csharp_style_expression_bodied_local_functions = false:warning 48 | csharp_style_throw_expression = true:suggestion 49 | csharp_style_prefer_null_check_over_type_check = true:suggestion 50 | csharp_prefer_simple_default_expression = true:suggestion 51 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 52 | csharp_style_prefer_index_operator = true:suggestion 53 | csharp_style_prefer_range_operator = true:suggestion 54 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 55 | csharp_space_around_binary_operators = before_and_after 56 | csharp_style_prefer_tuple_swap = true:suggestion 57 | csharp_style_prefer_utf8_string_literals = true:suggestion 58 | csharp_style_inlined_variable_declaration = true:suggestion 59 | csharp_style_deconstructed_variable_declaration = true:suggestion 60 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 61 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 62 | csharp_prefer_static_local_function = true:suggestion 63 | csharp_style_allow_embedded_statements_on_same_line_experimental = true:suggestion 64 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:suggestion 65 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:suggestion 66 | csharp_style_conditional_delegate_call = true:suggestion 67 | csharp_style_prefer_switch_expression = true:warning 68 | csharp_style_prefer_pattern_matching = true:suggestion 69 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 70 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 71 | csharp_style_prefer_not_pattern = true:suggestion 72 | csharp_style_prefer_extended_property_pattern = true:suggestion 73 | csharp_style_var_for_built_in_types = false:warning 74 | csharp_style_var_when_type_is_apparent = false:silent 75 | csharp_style_var_elsewhere = false:warning 76 | dotnet_diagnostic.CA1001.severity = error 77 | dotnet_diagnostic.CA1309.severity = suggestion 78 | dotnet_diagnostic.CA1507.severity = warning 79 | dotnet_diagnostic.CA1805.severity = warning 80 | dotnet_diagnostic.CA1825.severity = warning 81 | dotnet_diagnostic.CA1841.severity = warning 82 | dotnet_diagnostic.CA1845.severity = warning 83 | dotnet_diagnostic.CA2016.severity = warning 84 | dotnet_diagnostic.RCS1034.severity = error 85 | 86 | # Xml build files 87 | [*.builds] 88 | indent_size = 2 89 | 90 | # Xml files 91 | [*.{xml,stylecop,resx,ruleset}] 92 | indent_size = 2 93 | 94 | # Xml config files 95 | [*.{props,targets,config,nuspec}] 96 | indent_size = 2 97 | 98 | # Shell scripts 99 | [*.sh] 100 | end_of_line = lf 101 | 102 | [*.{cmd, bat}] 103 | end_of_line = crlf 104 | 105 | [*.{cs,vb}] 106 | #### Naming styles #### 107 | 108 | # Naming rules 109 | 110 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 111 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 112 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 113 | 114 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 115 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 116 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 117 | 118 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 119 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 120 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 121 | 122 | # Symbol specifications 123 | 124 | dotnet_naming_symbols.interface.applicable_kinds = interface 125 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 126 | dotnet_naming_symbols.interface.required_modifiers = 127 | 128 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 129 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 130 | dotnet_naming_symbols.types.required_modifiers = 131 | 132 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 133 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 134 | dotnet_naming_symbols.non_field_members.required_modifiers = 135 | 136 | # Naming styles 137 | 138 | dotnet_naming_style.begins_with_i.required_prefix = I 139 | dotnet_naming_style.begins_with_i.required_suffix = 140 | dotnet_naming_style.begins_with_i.word_separator = 141 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 142 | 143 | dotnet_naming_style.pascal_case.required_prefix = 144 | dotnet_naming_style.pascal_case.required_suffix = 145 | dotnet_naming_style.pascal_case.word_separator = 146 | dotnet_naming_style.pascal_case.capitalization = pascal_case 147 | 148 | dotnet_naming_style.pascal_case.required_prefix = 149 | dotnet_naming_style.pascal_case.required_suffix = 150 | dotnet_naming_style.pascal_case.word_separator = 151 | dotnet_naming_style.pascal_case.capitalization = pascal_case 152 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 153 | tab_width = 4 154 | end_of_line = crlf 155 | dotnet_style_coalesce_expression = true:suggestion 156 | dotnet_style_null_propagation = true:suggestion 157 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 158 | dotnet_style_prefer_auto_properties = true:silent 159 | dotnet_style_object_initializer = true:suggestion 160 | dotnet_style_collection_initializer = true:suggestion 161 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 162 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 163 | dotnet_style_prefer_conditional_expression_over_return = true:silent 164 | dotnet_style_explicit_tuple_names = true:suggestion 165 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 166 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 167 | dotnet_style_prefer_compound_assignment = true:suggestion 168 | dotnet_style_prefer_simplified_interpolation = true:suggestion 169 | dotnet_style_namespace_match_folder = true:suggestion 170 | dotnet_style_readonly_field = true:warning 171 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 172 | dotnet_style_predefined_type_for_member_access = true:silent 173 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 174 | dotnet_style_allow_multiple_blank_lines_experimental = true:warning 175 | dotnet_style_allow_statement_immediately_after_block_experimental = true:suggestion 176 | dotnet_code_quality_unused_parameters = all:suggestion 177 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 178 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 179 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 180 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 181 | dotnet_style_qualification_for_field = false:silent 182 | dotnet_style_qualification_for_property = false:silent 183 | dotnet_style_qualification_for_method = false:silent 184 | dotnet_style_qualification_for_event = false:silent 185 | dotnet_diagnostic.CA1018.severity = warning 186 | dotnet_diagnostic.CA1021.severity = suggestion 187 | dotnet_diagnostic.CA1036.severity = suggestion 188 | dotnet_diagnostic.CA1310.severity = suggestion 189 | dotnet_diagnostic.CA1305.severity = suggestion 190 | dotnet_diagnostic.CA1304.severity = suggestion 191 | dotnet_diagnostic.CA1821.severity = warning 192 | dotnet_diagnostic.CA1822.severity = warning 193 | dotnet_diagnostic.CA1826.severity = warning 194 | dotnet_diagnostic.CA1827.severity = warning 195 | dotnet_diagnostic.CA1828.severity = warning 196 | dotnet_diagnostic.CA1829.severity = warning 197 | dotnet_diagnostic.CA1830.severity = warning 198 | dotnet_diagnostic.CA1832.severity = warning 199 | dotnet_diagnostic.CA1833.severity = warning 200 | dotnet_diagnostic.CA1834.severity = warning 201 | dotnet_diagnostic.CA1835.severity = warning 202 | dotnet_diagnostic.CA1836.severity = warning 203 | dotnet_diagnostic.CA1842.severity = warning 204 | dotnet_diagnostic.CA1843.severity = warning 205 | dotnet_diagnostic.CA1844.severity = warning 206 | dotnet_diagnostic.CA1846.severity = warning 207 | dotnet_diagnostic.CA1847.severity = warning 208 | dotnet_diagnostic.CA2009.severity = warning 209 | dotnet_diagnostic.CA2011.severity = warning 210 | dotnet_diagnostic.CA2012.severity = warning 211 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Catch all for anything we forgot. Add rules if you get CRLF to LF warnings. 2 | * text=auto 3 | 4 | # Text files that should be normalized to LF in odb. 5 | *.cs text diff=csharp 6 | *.xaml text 7 | *.config text 8 | *.c text 9 | *.h text 10 | *.cpp text 11 | *.hpp text 12 | *.sln text 13 | *.csproj text 14 | *.vcxproj text 15 | *.md text 16 | *.tt text 17 | *.sh text 18 | *.ps1 text 19 | *.cmd text 20 | *.bat text 21 | *.markdown text 22 | *.msbuild text 23 | 24 | # Binary files that should not be normalized or diffed 25 | *.png binary 26 | *.jpg binary 27 | *.gif binary 28 | *.ico binary 29 | *.rc binary 30 | *.pfx binary 31 | *.snk binary 32 | *.dll binary 33 | *.exe binary 34 | *.lib binary 35 | *.exp binary 36 | *.pdb binary 37 | *.sdf binary 38 | *.7z binary 39 | 40 | # Generated file should just use CRLF, it's fiiine 41 | SolutionInfo.cs text eol=crlf diff=csharp 42 | *.mht filter=lfs diff=lfs merge=lfs -text 43 | *.ppam filter=lfs diff=lfs merge=lfs -text 44 | *.wmv filter=lfs diff=lfs merge=lfs -text 45 | *.btif filter=lfs diff=lfs merge=lfs -text 46 | *.fla filter=lfs diff=lfs merge=lfs -text 47 | *.qt filter=lfs diff=lfs merge=lfs -text 48 | *.xlam filter=lfs diff=lfs merge=lfs -text 49 | *.xm filter=lfs diff=lfs merge=lfs -text 50 | *.djvu filter=lfs diff=lfs merge=lfs -text 51 | *.woff filter=lfs diff=lfs merge=lfs -text 52 | *.a filter=lfs diff=lfs merge=lfs -text 53 | *.bak filter=lfs diff=lfs merge=lfs -text 54 | *.lha filter=lfs diff=lfs merge=lfs -text 55 | *.mpg filter=lfs diff=lfs merge=lfs -text 56 | *.xltm filter=lfs diff=lfs merge=lfs -text 57 | *.eol filter=lfs diff=lfs merge=lfs -text 58 | *.ipa filter=lfs diff=lfs merge=lfs -text 59 | *.ttf filter=lfs diff=lfs merge=lfs -text 60 | *.uvm filter=lfs diff=lfs merge=lfs -text 61 | *.cmx filter=lfs diff=lfs merge=lfs -text 62 | *.dng filter=lfs diff=lfs merge=lfs -text 63 | *.xltx filter=lfs diff=lfs merge=lfs -text 64 | *.fli filter=lfs diff=lfs merge=lfs -text 65 | *.wmx filter=lfs diff=lfs merge=lfs -text 66 | *.jxr filter=lfs diff=lfs merge=lfs -text 67 | *.pyv filter=lfs diff=lfs merge=lfs -text 68 | *.s7z filter=lfs diff=lfs merge=lfs -text 69 | *.csv filter=lfs diff=lfs merge=lfs -text 70 | *.pptm filter=lfs diff=lfs merge=lfs -text 71 | *.rz filter=lfs diff=lfs merge=lfs -text 72 | *.wm filter=lfs diff=lfs merge=lfs -text 73 | *.xlsx filter=lfs diff=lfs merge=lfs -text 74 | *.bh filter=lfs diff=lfs merge=lfs -text 75 | *.dat filter=lfs diff=lfs merge=lfs -text 76 | *.mid filter=lfs diff=lfs merge=lfs -text 77 | *.mpga filter=lfs diff=lfs merge=lfs -text 78 | *.ogg filter=lfs diff=lfs merge=lfs -text 79 | *.s3m filter=lfs diff=lfs merge=lfs -text 80 | *.mar filter=lfs diff=lfs merge=lfs -text 81 | *.movie filter=lfs diff=lfs merge=lfs -text 82 | *.pptx filter=lfs diff=lfs merge=lfs -text 83 | *.dll filter=lfs diff=lfs merge=lfs -text 84 | *.docm filter=lfs diff=lfs merge=lfs -text 85 | *.m3u filter=lfs diff=lfs merge=lfs -text 86 | *.mov filter=lfs diff=lfs merge=lfs -text 87 | *.aac filter=lfs diff=lfs merge=lfs -text 88 | *.jar filter=lfs diff=lfs merge=lfs -text 89 | *.midi filter=lfs diff=lfs merge=lfs -text 90 | *.mobi filter=lfs diff=lfs merge=lfs -text 91 | *.potm filter=lfs diff=lfs merge=lfs -text 92 | *.woff2 filter=lfs diff=lfs merge=lfs -text 93 | *.cab filter=lfs diff=lfs merge=lfs -text 94 | *.dmg filter=lfs diff=lfs merge=lfs -text 95 | *.pdf filter=lfs diff=lfs merge=lfs -text 96 | *.war filter=lfs diff=lfs merge=lfs -text 97 | *.bz2 filter=lfs diff=lfs merge=lfs -text 98 | *.icns filter=lfs diff=lfs merge=lfs -text 99 | *.slk filter=lfs diff=lfs merge=lfs -text 100 | *.wbmp filter=lfs diff=lfs merge=lfs -text 101 | *.xpm filter=lfs diff=lfs merge=lfs -text 102 | *.xmind filter=lfs diff=lfs merge=lfs -text 103 | *.3g2 filter=lfs diff=lfs merge=lfs -text 104 | *.m4v filter=lfs diff=lfs merge=lfs -text 105 | *.pic filter=lfs diff=lfs merge=lfs -text 106 | *.uvi filter=lfs diff=lfs merge=lfs -text 107 | *.uvp filter=lfs diff=lfs merge=lfs -text 108 | *.xls filter=lfs diff=lfs merge=lfs -text 109 | *.jpgv filter=lfs diff=lfs merge=lfs -text 110 | *.mka filter=lfs diff=lfs merge=lfs -text 111 | *.swf filter=lfs diff=lfs merge=lfs -text 112 | *.uvs filter=lfs diff=lfs merge=lfs -text 113 | *.wav filter=lfs diff=lfs merge=lfs -text 114 | *.ecelp4800 filter=lfs diff=lfs merge=lfs -text 115 | *.mng filter=lfs diff=lfs merge=lfs -text 116 | *.pps filter=lfs diff=lfs merge=lfs -text 117 | *.whl filter=lfs diff=lfs merge=lfs -text 118 | *.arj filter=lfs diff=lfs merge=lfs -text 119 | *.lzh filter=lfs diff=lfs merge=lfs -text 120 | *.raw filter=lfs diff=lfs merge=lfs -text 121 | *.rlc filter=lfs diff=lfs merge=lfs -text 122 | *.sgi filter=lfs diff=lfs merge=lfs -text 123 | *.tar filter=lfs diff=lfs merge=lfs -text 124 | *.au filter=lfs diff=lfs merge=lfs -text 125 | *.dcm filter=lfs diff=lfs merge=lfs -text 126 | *.GIF filter=lfs diff=lfs merge=lfs -text 127 | *.resources filter=lfs diff=lfs merge=lfs -text 128 | *.txz filter=lfs diff=lfs merge=lfs -text 129 | *.rar filter=lfs diff=lfs merge=lfs -text 130 | *.sil filter=lfs diff=lfs merge=lfs -text 131 | *.bk filter=lfs diff=lfs merge=lfs -text 132 | *.DS_Store filter=lfs diff=lfs merge=lfs -text 133 | *.ief filter=lfs diff=lfs merge=lfs -text 134 | *.JPEG filter=lfs diff=lfs merge=lfs -text 135 | *.pbm filter=lfs diff=lfs merge=lfs -text 136 | *.png filter=lfs diff=lfs merge=lfs -text 137 | *.sketch filter=lfs diff=lfs merge=lfs -text 138 | *.tbz2 filter=lfs diff=lfs merge=lfs -text 139 | *.nef filter=lfs diff=lfs merge=lfs -text 140 | *.oga filter=lfs diff=lfs merge=lfs -text 141 | *.zip filter=lfs diff=lfs merge=lfs -text 142 | *.ecelp7470 filter=lfs diff=lfs merge=lfs -text 143 | *.xlt filter=lfs diff=lfs merge=lfs -text 144 | *.exe filter=lfs diff=lfs merge=lfs -text 145 | *.mp4 filter=lfs diff=lfs merge=lfs -text 146 | *.pnm filter=lfs diff=lfs merge=lfs -text 147 | *.ttc filter=lfs diff=lfs merge=lfs -text 148 | *.wdp filter=lfs diff=lfs merge=lfs -text 149 | *.xbm filter=lfs diff=lfs merge=lfs -text 150 | *.ecelp9600 filter=lfs diff=lfs merge=lfs -text 151 | *.pot filter=lfs diff=lfs merge=lfs -text 152 | *.wvx filter=lfs diff=lfs merge=lfs -text 153 | *.uvu filter=lfs diff=lfs merge=lfs -text 154 | *.asf filter=lfs diff=lfs merge=lfs -text 155 | *.dxf filter=lfs diff=lfs merge=lfs -text 156 | *.flv filter=lfs diff=lfs merge=lfs -text 157 | *.mdi filter=lfs diff=lfs merge=lfs -text 158 | *.pcx filter=lfs diff=lfs merge=lfs -text 159 | *.tiff filter=lfs diff=lfs merge=lfs -text 160 | *.bzip2 filter=lfs diff=lfs merge=lfs -text 161 | *.deb filter=lfs diff=lfs merge=lfs -text 162 | *.graffle filter=lfs diff=lfs merge=lfs -text 163 | *.h261 filter=lfs diff=lfs merge=lfs -text 164 | *.jpeg filter=lfs diff=lfs merge=lfs -text 165 | *.ppm filter=lfs diff=lfs merge=lfs -text 166 | *.tif filter=lfs diff=lfs merge=lfs -text 167 | *.ppt filter=lfs diff=lfs merge=lfs -text 168 | *.fbs filter=lfs diff=lfs merge=lfs -text 169 | *.gzip filter=lfs diff=lfs merge=lfs -text 170 | *.o filter=lfs diff=lfs merge=lfs -text 171 | *.sub filter=lfs diff=lfs merge=lfs -text 172 | *.z filter=lfs diff=lfs merge=lfs -text 173 | *.alz filter=lfs diff=lfs merge=lfs -text 174 | *.BMP filter=lfs diff=lfs merge=lfs -text 175 | *.dotm filter=lfs diff=lfs merge=lfs -text 176 | *.key filter=lfs diff=lfs merge=lfs -text 177 | *.rgb filter=lfs diff=lfs merge=lfs -text 178 | *.f4v filter=lfs diff=lfs merge=lfs -text 179 | *.iso filter=lfs diff=lfs merge=lfs -text 180 | *.ai filter=lfs diff=lfs merge=lfs -text 181 | *.dtshd filter=lfs diff=lfs merge=lfs -text 182 | *.fpx filter=lfs diff=lfs merge=lfs -text 183 | *.shar filter=lfs diff=lfs merge=lfs -text 184 | *.img filter=lfs diff=lfs merge=lfs -text 185 | *.rmf filter=lfs diff=lfs merge=lfs -text 186 | *.xz filter=lfs diff=lfs merge=lfs -text 187 | *.eot filter=lfs diff=lfs merge=lfs -text 188 | *.wma filter=lfs diff=lfs merge=lfs -text 189 | *.cpio filter=lfs diff=lfs merge=lfs -text 190 | *.cr2 filter=lfs diff=lfs merge=lfs -text 191 | *.adp filter=lfs diff=lfs merge=lfs -text 192 | *.mpeg filter=lfs diff=lfs merge=lfs -text 193 | *.npx filter=lfs diff=lfs merge=lfs -text 194 | *.pdb filter=lfs diff=lfs merge=lfs -text 195 | *.PNG filter=lfs diff=lfs merge=lfs -text 196 | *.xwd filter=lfs diff=lfs merge=lfs -text 197 | *.egg filter=lfs diff=lfs merge=lfs -text 198 | *.ppsx filter=lfs diff=lfs merge=lfs -text 199 | *.mp4a filter=lfs diff=lfs merge=lfs -text 200 | *.pages filter=lfs diff=lfs merge=lfs -text 201 | *.baml filter=lfs diff=lfs merge=lfs -text 202 | *.bin filter=lfs diff=lfs merge=lfs -text 203 | *.class filter=lfs diff=lfs merge=lfs -text 204 | *.h264 filter=lfs diff=lfs merge=lfs -text 205 | *.lib filter=lfs diff=lfs merge=lfs -text 206 | *.mmr filter=lfs diff=lfs merge=lfs -text 207 | *.dot filter=lfs diff=lfs merge=lfs -text 208 | *.gif filter=lfs diff=lfs merge=lfs -text 209 | *.JPG filter=lfs diff=lfs merge=lfs -text 210 | *.m4a filter=lfs diff=lfs merge=lfs -text 211 | *.so filter=lfs diff=lfs merge=lfs -text 212 | *.tgz filter=lfs diff=lfs merge=lfs -text 213 | *.thmx filter=lfs diff=lfs merge=lfs -text 214 | *.3ds filter=lfs diff=lfs merge=lfs -text 215 | *.bmp filter=lfs diff=lfs merge=lfs -text 216 | *.ogv filter=lfs diff=lfs merge=lfs -text 217 | *.xif filter=lfs diff=lfs merge=lfs -text 218 | *.aiff filter=lfs diff=lfs merge=lfs -text 219 | *.dts filter=lfs diff=lfs merge=lfs -text 220 | *.rip filter=lfs diff=lfs merge=lfs -text 221 | *.vob filter=lfs diff=lfs merge=lfs -text 222 | *.7z filter=lfs diff=lfs merge=lfs -text 223 | *.fh filter=lfs diff=lfs merge=lfs -text 224 | *.flac filter=lfs diff=lfs merge=lfs -text 225 | *.g3 filter=lfs diff=lfs merge=lfs -text 226 | *.jpm filter=lfs diff=lfs merge=lfs -text 227 | *.ppsm filter=lfs diff=lfs merge=lfs -text 228 | *.potx filter=lfs diff=lfs merge=lfs -text 229 | *.zipx filter=lfs diff=lfs merge=lfs -text 230 | *.dsk filter=lfs diff=lfs merge=lfs -text 231 | *.ico filter=lfs diff=lfs merge=lfs -text 232 | *.ktx filter=lfs diff=lfs merge=lfs -text 233 | *.lz filter=lfs diff=lfs merge=lfs -text 234 | *.numbers filter=lfs diff=lfs merge=lfs -text 235 | *.3gp filter=lfs diff=lfs merge=lfs -text 236 | *.fst filter=lfs diff=lfs merge=lfs -text 237 | *.scpt filter=lfs diff=lfs merge=lfs -text 238 | *.epub filter=lfs diff=lfs merge=lfs -text 239 | *.rmvb filter=lfs diff=lfs merge=lfs -text 240 | *.webm filter=lfs diff=lfs merge=lfs -text 241 | *.docx filter=lfs diff=lfs merge=lfs -text 242 | *.pgm filter=lfs diff=lfs merge=lfs -text 243 | *.pya filter=lfs diff=lfs merge=lfs -text 244 | *.rtf filter=lfs diff=lfs merge=lfs -text 245 | *.smv filter=lfs diff=lfs merge=lfs -text 246 | *.tga filter=lfs diff=lfs merge=lfs -text 247 | *.cur filter=lfs diff=lfs merge=lfs -text 248 | *.dwg filter=lfs diff=lfs merge=lfs -text 249 | *.lvp filter=lfs diff=lfs merge=lfs -text 250 | *.pyo filter=lfs diff=lfs merge=lfs -text 251 | *.apk filter=lfs diff=lfs merge=lfs -text 252 | *.ar filter=lfs diff=lfs merge=lfs -text 253 | *.caf filter=lfs diff=lfs merge=lfs -text 254 | *.doc filter=lfs diff=lfs merge=lfs -text 255 | *.h263 filter=lfs diff=lfs merge=lfs -text 256 | *.xlsm filter=lfs diff=lfs merge=lfs -text 257 | *.mp3 filter=lfs diff=lfs merge=lfs -text 258 | *.mxu filter=lfs diff=lfs merge=lfs -text 259 | *.wax filter=lfs diff=lfs merge=lfs -text 260 | *.gz filter=lfs diff=lfs merge=lfs -text 261 | *.mj2 filter=lfs diff=lfs merge=lfs -text 262 | *.otf filter=lfs diff=lfs merge=lfs -text 263 | *.udf filter=lfs diff=lfs merge=lfs -text 264 | *.aif filter=lfs diff=lfs merge=lfs -text 265 | *.lzma filter=lfs diff=lfs merge=lfs -text 266 | *.pyc filter=lfs diff=lfs merge=lfs -text 267 | *.weba filter=lfs diff=lfs merge=lfs -text 268 | *.webp filter=lfs diff=lfs merge=lfs -text 269 | *.cgm filter=lfs diff=lfs merge=lfs -text 270 | *.mkv filter=lfs diff=lfs merge=lfs -text 271 | *.ppa filter=lfs diff=lfs merge=lfs -text 272 | *.uvh filter=lfs diff=lfs merge=lfs -text 273 | *.xpi filter=lfs diff=lfs merge=lfs -text 274 | *.psd filter=lfs diff=lfs merge=lfs -text 275 | *.xlsb filter=lfs diff=lfs merge=lfs -text 276 | *.tbz filter=lfs diff=lfs merge=lfs -text 277 | *.wim filter=lfs diff=lfs merge=lfs -text 278 | *.ape filter=lfs diff=lfs merge=lfs -text 279 | *.avi filter=lfs diff=lfs merge=lfs -text 280 | *.dex filter=lfs diff=lfs merge=lfs -text 281 | *.dra filter=lfs diff=lfs merge=lfs -text 282 | *.dvb filter=lfs diff=lfs merge=lfs -text 283 | *.jpg filter=lfs diff=lfs merge=lfs -text 284 | *.xla filter=lfs diff=lfs merge=lfs -text 285 | *.fvt filter=lfs diff=lfs merge=lfs -text 286 | *.lzo filter=lfs diff=lfs merge=lfs -text 287 | *.pea filter=lfs diff=lfs merge=lfs -text 288 | *.ras filter=lfs diff=lfs merge=lfs -text 289 | *.tlz filter=lfs diff=lfs merge=lfs -text 290 | *.viv filter=lfs diff=lfs merge=lfs -text 291 | *.winmd filter=lfs diff=lfs merge=lfs -text 292 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["local>reactivemarbles/.github:renovate"] 4 | } -------------------------------------------------------------------------------- /.github/workflows/ci-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | configuration: Release 11 | productNamespacePrefix: "ReactiveMarbles" 12 | 13 | jobs: 14 | build: 15 | runs-on: windows-2022 16 | outputs: 17 | nbgv: ${{ steps.nbgv.outputs.SemVer2 }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4.1.7 21 | with: 22 | fetch-depth: 0 23 | lfs: true 24 | 25 | - name: Install .NET 26 | uses: actions/setup-dotnet@v4.0.1 27 | with: 28 | dotnet-version: | 29 | 6.0.x 30 | 7.0.x 31 | 8.0.x 32 | 33 | - name: NBGV 34 | id: nbgv 35 | uses: dotnet/nbgv@master 36 | with: 37 | setAllVars: true 38 | 39 | - name: NuGet Restore 40 | run: dotnet restore 41 | working-directory: src 42 | 43 | - name: Build 44 | run: dotnet build --configuration=${{ env.configuration }} --verbosity=minimal --no-restore 45 | working-directory: src 46 | 47 | - name: Run Unit Tests and Generate Coverage 48 | uses: glennawatson/coverlet-msbuild@v2 49 | with: 50 | project-files: '**/*Tests*.csproj' 51 | no-build: true 52 | exclude-filter: '[${{env.productNamespacePrefix}}.*.Tests.*]*' 53 | include-filter: '[${{env.productNamespacePrefix}}*]*' 54 | output-format: cobertura 55 | output: '../../artifacts/' 56 | configuration: ${{ env.configuration }} 57 | 58 | - name: Pack 59 | run: dotnet pack --configuration=${{ env.configuration }} --verbosity=minimal --no-restore 60 | working-directory: src 61 | 62 | - name: Upload Code Coverage 63 | shell: bash 64 | run: | 65 | echo $PWD 66 | bash <(curl -s https://codecov.io/bash) -X gcov -X coveragepy -t ${{ env.CODECOV_TOKEN }} -s '$PWD/artifacts' -f '*.xml' 67 | env: 68 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 69 | 70 | - name: Create NuGet Artifacts 71 | uses: actions/upload-artifact@master 72 | with: 73 | name: nuget 74 | path: '**/*.nupkg' 75 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: 'Lock Threads' 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | workflow_dispatch: 7 | 8 | permissions: 9 | issues: write 10 | pull-requests: write 11 | 12 | concurrency: 13 | group: lock 14 | 15 | jobs: 16 | action: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: dessant/lock-threads@v5 20 | with: 21 | github-token: ${{ github.token }} 22 | issue-inactive-days: '14' 23 | pr-inactive-days: '14' 24 | issue-comment: > 25 | This issue has been automatically locked since there 26 | has not been any recent activity after it was closed. 27 | Please open a new issue for related bugs. 28 | pr-comment: > 29 | This pull request has been automatically locked since there 30 | has not been any recent activity after it was closed. 31 | Please open a new issue for related bugs. -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | env: 8 | configuration: Release 9 | productNamespacePrefix: "ReactiveMarbles" 10 | 11 | jobs: 12 | release: 13 | runs-on: windows-2022 14 | environment: 15 | name: release 16 | outputs: 17 | nbgv: ${{ steps.nbgv.outputs.SemVer2 }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4.1.7 21 | with: 22 | fetch-depth: 0 23 | lfs: true 24 | 25 | - name: Install .NET 26 | uses: actions/setup-dotnet@v4.0.1 27 | with: 28 | dotnet-version: | 29 | 6.0.x 30 | 7.0.x 31 | 8.0.x 32 | 33 | - name: NBGV 34 | id: nbgv 35 | uses: dotnet/nbgv@master 36 | with: 37 | setAllVars: true 38 | 39 | - name: NuGet Restore 40 | run: dotnet restore 41 | working-directory: src 42 | 43 | - name: Build 44 | run: dotnet build --configuration=${{ env.configuration }} --verbosity=minimal --no-restore 45 | working-directory: src 46 | 47 | - uses: nuget/setup-nuget@v2 48 | name: Setup NuGet 49 | 50 | - name: Pack 51 | run: dotnet pack --configuration=${{ env.configuration }} --verbosity=minimal --no-restore 52 | working-directory: src 53 | 54 | # Decode the base 64 encoded pfx and save the Signing_Certificate 55 | - name: Sign NuGet packages 56 | shell: pwsh 57 | run: | 58 | $pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.SIGNING_CERTIFICATE }}") 59 | [IO.File]::WriteAllBytes("GitHubActionsWorkflow.pfx", $pfx_cert_byte) 60 | $secure_password = ConvertTo-SecureString ${{ secrets.SIGN_CERTIFICATE_PASSWORD }} –asplaintext –force 61 | Import-PfxCertificate -FilePath GitHubActionsWorkflow.pfx -Password $secure_password -CertStoreLocation Cert:\CurrentUser\My 62 | nuget sign -Timestamper http://timestamp.digicert.com -CertificateFingerprint ${{ secrets.SIGN_CERTIFICATE_HASH }} **/*.nupkg 63 | 64 | - name: Changelog 65 | uses: glennawatson/ChangeLog@v1 66 | id: changelog 67 | 68 | - name: Create Release 69 | uses: actions/create-release@v1.1.4 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 72 | with: 73 | tag_name: ${{ steps.nbgv.outputs.SemVer2 }} 74 | release_name: ${{ steps.nbgv.outputs.SemVer2 }} 75 | body: | 76 | ${{ steps.changelog.outputs.commitLog }} 77 | 78 | - name: NuGet Push 79 | env: 80 | NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY }} 81 | SOURCE_URL: https://api.nuget.org/v3/index.json 82 | run: | 83 | dotnet nuget push -s ${{ env.SOURCE_URL }} -k ${{ env.NUGET_AUTH_TOKEN }} **/*.nupkg 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # ASP.NET Scaffolding 66 | ScaffoldingReadMe.txt 67 | 68 | # StyleCop 69 | StyleCopReport.xml 70 | 71 | # Files built by Visual Studio 72 | *_i.c 73 | *_p.c 74 | *_h.h 75 | *.ilk 76 | *.meta 77 | *.obj 78 | *.iobj 79 | *.pch 80 | *.pdb 81 | *.ipdb 82 | *.pgc 83 | *.pgd 84 | *.rsp 85 | *.sbr 86 | *.tlb 87 | *.tli 88 | *.tlh 89 | *.tmp 90 | *.tmp_proj 91 | *_wpftmp.csproj 92 | *.log 93 | *.vspscc 94 | *.vssscc 95 | .builds 96 | *.pidb 97 | *.svclog 98 | *.scc 99 | 100 | # Chutzpah Test files 101 | _Chutzpah* 102 | 103 | # Visual C++ cache files 104 | ipch/ 105 | *.aps 106 | *.ncb 107 | *.opendb 108 | *.opensdf 109 | *.sdf 110 | *.cachefile 111 | *.VC.db 112 | *.VC.VC.opendb 113 | 114 | # Visual Studio profiler 115 | *.psess 116 | *.vsp 117 | *.vspx 118 | *.sap 119 | 120 | # Visual Studio Trace Files 121 | *.e2e 122 | 123 | # TFS 2012 Local Workspace 124 | $tf/ 125 | 126 | # Guidance Automation Toolkit 127 | *.gpState 128 | 129 | # ReSharper is a .NET coding add-in 130 | _ReSharper*/ 131 | *.[Rr]e[Ss]harper 132 | *.DotSettings.user 133 | 134 | # TeamCity is a build add-in 135 | _TeamCity* 136 | 137 | # DotCover is a Code Coverage Tool 138 | *.dotCover 139 | 140 | # AxoCover is a Code Coverage Tool 141 | .axoCover/* 142 | !.axoCover/settings.json 143 | 144 | # Coverlet is a free, cross platform Code Coverage Tool 145 | coverage*.json 146 | coverage*.xml 147 | coverage*.info 148 | 149 | # Visual Studio code coverage results 150 | *.coverage 151 | *.coveragexml 152 | 153 | # NCrunch 154 | _NCrunch_* 155 | .*crunch*.local.xml 156 | nCrunchTemp_* 157 | 158 | # MightyMoose 159 | *.mm.* 160 | AutoTest.Net/ 161 | 162 | # Web workbench (sass) 163 | .sass-cache/ 164 | 165 | # Installshield output folder 166 | [Ee]xpress/ 167 | 168 | # DocProject is a documentation generator add-in 169 | DocProject/buildhelp/ 170 | DocProject/Help/*.HxT 171 | DocProject/Help/*.HxC 172 | DocProject/Help/*.hhc 173 | DocProject/Help/*.hhk 174 | DocProject/Help/*.hhp 175 | DocProject/Help/Html2 176 | DocProject/Help/html 177 | 178 | # Click-Once directory 179 | publish/ 180 | 181 | # Publish Web Output 182 | *.[Pp]ublish.xml 183 | *.azurePubxml 184 | # Note: Comment the next line if you want to checkin your web deploy settings, 185 | # but database connection strings (with potential passwords) will be unencrypted 186 | *.pubxml 187 | *.publishproj 188 | 189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 190 | # checkin your Azure Web App publish settings, but sensitive information contained 191 | # in these scripts will be unencrypted 192 | PublishScripts/ 193 | 194 | # NuGet Packages 195 | *.nupkg 196 | # NuGet Symbol Packages 197 | *.snupkg 198 | # The packages folder can be ignored because of Package Restore 199 | **/[Pp]ackages/* 200 | # except build/, which is used as an MSBuild target. 201 | !**/[Pp]ackages/build/ 202 | # Uncomment if necessary however generally it will be regenerated when needed 203 | #!**/[Pp]ackages/repositories.config 204 | # NuGet v3's project.json files produces more ignorable files 205 | *.nuget.props 206 | *.nuget.targets 207 | 208 | # Microsoft Azure Build Output 209 | csx/ 210 | *.build.csdef 211 | 212 | # Microsoft Azure Emulator 213 | ecf/ 214 | rcf/ 215 | 216 | # Windows Store app package directories and files 217 | AppPackages/ 218 | BundleArtifacts/ 219 | Package.StoreAssociation.xml 220 | _pkginfo.txt 221 | *.appx 222 | *.appxbundle 223 | *.appxupload 224 | 225 | # Visual Studio cache files 226 | # files ending in .cache can be ignored 227 | *.[Cc]ache 228 | # but keep track of directories ending in .cache 229 | !?*.[Cc]ache/ 230 | 231 | # Others 232 | ClientBin/ 233 | ~$* 234 | *~ 235 | *.dbmdl 236 | *.dbproj.schemaview 237 | *.jfm 238 | *.pfx 239 | *.publishsettings 240 | orleans.codegen.cs 241 | 242 | # Including strong name files can present a security risk 243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 244 | #*.snk 245 | 246 | # Since there are multiple workflows, uncomment next line to ignore bower_components 247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 248 | #bower_components/ 249 | 250 | # RIA/Silverlight projects 251 | Generated_Code/ 252 | 253 | # Backup & report files from converting an old project file 254 | # to a newer Visual Studio version. Backup files are not needed, 255 | # because we have git ;-) 256 | _UpgradeReport_Files/ 257 | Backup*/ 258 | UpgradeLog*.XML 259 | UpgradeLog*.htm 260 | ServiceFabricBackup/ 261 | *.rptproj.bak 262 | 263 | # SQL Server files 264 | *.mdf 265 | *.ldf 266 | *.ndf 267 | 268 | # Business Intelligence projects 269 | *.rdl.data 270 | *.bim.layout 271 | *.bim_*.settings 272 | *.rptproj.rsuser 273 | *- [Bb]ackup.rdl 274 | *- [Bb]ackup ([0-9]).rdl 275 | *- [Bb]ackup ([0-9][0-9]).rdl 276 | 277 | # Microsoft Fakes 278 | FakesAssemblies/ 279 | 280 | # GhostDoc plugin setting file 281 | *.GhostDoc.xml 282 | 283 | # Node.js Tools for Visual Studio 284 | .ntvs_analysis.dat 285 | node_modules/ 286 | 287 | # Visual Studio 6 build log 288 | *.plg 289 | 290 | # Visual Studio 6 workspace options file 291 | *.opt 292 | 293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 294 | *.vbw 295 | 296 | # Visual Studio LightSwitch build output 297 | **/*.HTMLClient/GeneratedArtifacts 298 | **/*.DesktopClient/GeneratedArtifacts 299 | **/*.DesktopClient/ModelManifest.xml 300 | **/*.Server/GeneratedArtifacts 301 | **/*.Server/ModelManifest.xml 302 | _Pvt_Extensions 303 | 304 | # Paket dependency manager 305 | .paket/paket.exe 306 | paket-files/ 307 | 308 | # FAKE - F# Make 309 | .fake/ 310 | 311 | # CodeRush personal settings 312 | .cr/personal 313 | 314 | # Python Tools for Visual Studio (PTVS) 315 | __pycache__/ 316 | *.pyc 317 | 318 | # Cake - Uncomment if you are using it 319 | # tools/** 320 | # !tools/packages.config 321 | 322 | # Tabs Studio 323 | *.tss 324 | 325 | # Telerik's JustMock configuration file 326 | *.jmconfig 327 | 328 | # BizTalk build output 329 | *.btp.cs 330 | *.btm.cs 331 | *.odx.cs 332 | *.xsd.cs 333 | 334 | # OpenCover UI analysis results 335 | OpenCover/ 336 | 337 | # Azure Stream Analytics local run output 338 | ASALocalRun/ 339 | 340 | # MSBuild Binary and Structured Log 341 | *.binlog 342 | 343 | # NVidia Nsight GPU debugger configuration file 344 | *.nvuser 345 | 346 | # MFractors (Xamarin productivity tool) working folder 347 | .mfractor/ 348 | 349 | # Local History for Visual Studio 350 | .localhistory/ 351 | 352 | # BeatPulse healthcheck temp database 353 | healthchecksdb 354 | 355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 356 | MigrationBackup/ 357 | 358 | # Ionide (cross platform F# VS Code tools) working folder 359 | .ionide/ 360 | 361 | # Fody - auto-generated XML schema 362 | FodyWeavers.xsd 363 | 364 | .idea/ 365 | 366 | .DS_STORE 367 | 368 | MSBUILD_Logs 369 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | email hello@reactiveui.net. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ReactiveUI Association, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![License](https://img.shields.io/github/license/ReactiveMarbles/Mvvm.svg) [![Build](https://github.com/reactivemarbles/Mvvm/actions/workflows/ci-build.yml/badge.svg)](https://github.com/reactivemarbles/Mvvm/actions/workflows/ci-build.yml) 2 | 3 | 4 | # Reactive Marbles Mvvm 5 | A light weight mvvm package for Reactive Marbles to introduce framework abstractions 6 | 7 | ## NuGet packages 8 | 9 | | Name | Platform | NuGet | 10 | | ----------------------------- | ----------------- | -------------------------------- | 11 | | [ReactiveMarbles.Mvvm][Core] | Core - Libary | [![CoreBadge]][Core] | 12 | 13 | [Core]: https://www.nuget.org/packages/ReactiveMarbles.Mvvm/ 14 | [CoreBadge]: https://img.shields.io/nuget/v/ReactiveMarbles.Mvvm.svg 15 | 16 | ## Get Started 17 | 18 | ### Registering Framework Concerns 19 | 20 | `ICoreRegistration` gives the framework an understanding of the following concerns for ReactiveMarbles internals. We provide a simple builder and extension method to register it against the `ServiceLocator`. 21 | 22 | ```csharp 23 | ServiceLocator 24 | .Current() 25 | .AddCoreRegistrations(() => 26 | CoreRegistrationBuilder 27 | .Create() 28 | .WithMainThreadScheduler(Scheduler.Default) 29 | .WithTaskPoolScheduler(TaskPoolScheduler.Default) 30 | .WithExceptionHandler(new DebugExceptionHandler()) 31 | .Build()); 32 | ``` 33 | 34 | ### AsValue 35 | 36 | `AsValue` allows you to bind an `IObservable` to a property that produces a property changed event. 37 | 38 | ```csharp 39 | _valueChange = 40 | this.WhenChanged(x => x.Property) 41 | .Select(x => x + "Changed") 42 | .AsValue(onChanged: x => RaisePropertyChanged(nameof(ValueChange))); 43 | ``` 44 | 45 | ## Benchmarks 46 | 47 | To see how Mvvm compares to other frameworks see: [Benchmarks](https://github.com/reactivemarbles/Mvvm/blob/main/src/ReactiveMarbles.Mvvm.Benchmarks/README.MD) 48 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:722b25de1d0f7577e122a4d96c9d96f4019f7a34283a3612ae244a68eea7fc17 3 | size 47204 4 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # SA1101: Prefix local calls with this 4 | dotnet_diagnostic.SA1101.severity = none 5 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | # override for benchmarks. 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/Memory/DummyReactiveObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reactive.Linq; 8 | using System.Runtime.Serialization; 9 | 10 | using DynamicData.Binding; 11 | 12 | using ReactiveUI; 13 | 14 | namespace ReactiveMarbles.Mvvm.Benchmarks.Memory; 15 | 16 | /// 17 | /// Dummy object for the reactive object for tests. 18 | /// 19 | public class DummyReactiveObject : ReactiveObject 20 | { 21 | private readonly ObservableAsPropertyHelper _observableProperty; 22 | 23 | [IgnoreDataMember] 24 | private string? _isNotNullString; 25 | 26 | [IgnoreDataMember] 27 | private string? _isOnlyOneWord; 28 | 29 | private string? _notSerialized; 30 | 31 | [IgnoreDataMember] 32 | private int? _nullableInt; 33 | 34 | [IgnoreDataMember] 35 | private string? _pocoProperty; 36 | 37 | [IgnoreDataMember] 38 | private List? _stackOverflowTrigger; 39 | 40 | [IgnoreDataMember] 41 | private string? _usesExprRaiseSet; 42 | 43 | /// 44 | /// Initializes a new instance of the class. 45 | /// 46 | public DummyReactiveObject() 47 | { 48 | TestCollection = new ObservableCollectionExtended(); 49 | _observableProperty = 50 | this.WhenAnyValue(x => x.IsOnlyOneWord) 51 | .Select(x => x + "Changed") 52 | .ToProperty(this, nameof(ObservableProperty)); 53 | } 54 | 55 | /// 56 | /// Gets or sets the is not null string. 57 | /// 58 | [DataMember] 59 | public string? IsNotNullString 60 | { 61 | get => _isNotNullString; 62 | set => this.RaiseAndSetIfChanged(ref _isNotNullString, value); 63 | } 64 | 65 | /// 66 | /// Gets or sets the is only one word. 67 | /// 68 | [DataMember] 69 | public string? IsOnlyOneWord 70 | { 71 | get => _isOnlyOneWord; 72 | set => this.RaiseAndSetIfChanged(ref _isOnlyOneWord, value); 73 | } 74 | 75 | /// 76 | /// Gets or sets the not serialized. 77 | /// 78 | public string? NotSerialized 79 | { 80 | get => _notSerialized; 81 | set => this.RaiseAndSetIfChanged(ref _notSerialized, value); 82 | } 83 | 84 | /// 85 | /// Gets or sets the nullable int. 86 | /// 87 | [DataMember] 88 | public int? NullableInt 89 | { 90 | get => _nullableInt; 91 | set => this.RaiseAndSetIfChanged(ref _nullableInt, value); 92 | } 93 | 94 | /// 95 | /// Gets or sets the poco property. 96 | /// 97 | [DataMember] 98 | public string? PocoProperty 99 | { 100 | get => _pocoProperty; 101 | set => _pocoProperty = value; 102 | } 103 | 104 | /// 105 | /// Gets or sets the stack overflow trigger. 106 | /// 107 | [DataMember] 108 | public List? StackOverflowTrigger 109 | { 110 | get => _stackOverflowTrigger; 111 | set => this.RaiseAndSetIfChanged(ref _stackOverflowTrigger, value?.ToList()); 112 | } 113 | 114 | /// 115 | /// Gets or sets the test collection. 116 | /// 117 | [DataMember] 118 | public ObservableCollectionExtended TestCollection { get; protected set; } 119 | 120 | /// 121 | /// Gets or sets the uses expr raise set. 122 | /// 123 | [DataMember] 124 | public string? UsesExprRaiseSet 125 | { 126 | get => _usesExprRaiseSet; 127 | set => this.RaiseAndSetIfChanged(ref _usesExprRaiseSet, value); 128 | } 129 | 130 | /// 131 | /// Gets the observable property. 132 | /// 133 | public string ObservableProperty => _observableProperty.Value; 134 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/Memory/DummyRxObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reactive.Linq; 8 | using System.Runtime.Serialization; 9 | 10 | using DynamicData.Binding; 11 | 12 | using ReactiveMarbles.PropertyChanged; 13 | 14 | namespace ReactiveMarbles.Mvvm.Benchmarks.Memory; 15 | 16 | /// 17 | /// A dummy rx object. 18 | /// 19 | public class DummyRxObject : RxObject 20 | { 21 | private readonly IValueBinder _observableProperty; 22 | 23 | [IgnoreDataMember] 24 | private string? _isNotNullString; 25 | 26 | [IgnoreDataMember] 27 | private string? _isOnlyOneWord; 28 | 29 | private string? _notSerialized; 30 | 31 | [IgnoreDataMember] 32 | private int? _nullableInt; 33 | 34 | [IgnoreDataMember] 35 | private string? _pocoProperty; 36 | 37 | [IgnoreDataMember] 38 | private List? _stackOverflowTrigger; 39 | 40 | [IgnoreDataMember] 41 | private string? _usesExprRaiseSet; 42 | 43 | /// 44 | /// Initializes a new instance of the class. 45 | /// 46 | public DummyRxObject() 47 | { 48 | TestCollection = new ObservableCollectionExtended(); 49 | 50 | _observableProperty = 51 | this.WhenChanged(x => x.IsOnlyOneWord) 52 | .Select(x => x + "Changed") 53 | .AsValue(onChanged: x => RaisePropertyChanged(nameof(ObservableProperty))); 54 | } 55 | 56 | /// 57 | /// Gets or sets the is not null string. 58 | /// 59 | [DataMember] 60 | public string? IsNotNullString 61 | { 62 | get => _isNotNullString; 63 | set => RaiseAndSetIfChanged(ref _isNotNullString, value); 64 | } 65 | 66 | /// 67 | /// Gets or sets the is only one word. 68 | /// 69 | [DataMember] 70 | public string? IsOnlyOneWord 71 | { 72 | get => _isOnlyOneWord; 73 | set => RaiseAndSetIfChanged(ref _isOnlyOneWord, value); 74 | } 75 | 76 | /// 77 | /// Gets or sets the not serialized. 78 | /// 79 | public string? NotSerialized 80 | { 81 | get => _notSerialized; 82 | set => RaiseAndSetIfChanged(ref _notSerialized, value); 83 | } 84 | 85 | /// 86 | /// Gets or sets the nullable int. 87 | /// 88 | [DataMember] 89 | public int? NullableInt 90 | { 91 | get => _nullableInt; 92 | set => RaiseAndSetIfChanged(ref _nullableInt, value); 93 | } 94 | 95 | /// 96 | /// Gets or sets the poco property. 97 | /// 98 | [DataMember] 99 | public string? PocoProperty 100 | { 101 | get => _pocoProperty; 102 | set => _pocoProperty = value; 103 | } 104 | 105 | /// 106 | /// Gets or sets the stack overflow trigger. 107 | /// 108 | [DataMember] 109 | public List? StackOverflowTrigger 110 | { 111 | get => _stackOverflowTrigger; 112 | set => RaiseAndSetIfChanged(ref _stackOverflowTrigger, value?.ToList()); 113 | } 114 | 115 | /// 116 | /// Gets or sets the test collection. 117 | /// 118 | [DataMember] 119 | public ObservableCollectionExtended TestCollection { get; protected set; } 120 | 121 | /// 122 | /// Gets or sets the uses expr raise set. 123 | /// 124 | [DataMember] 125 | public string? UsesExprRaiseSet 126 | { 127 | get => _usesExprRaiseSet; 128 | set => RaiseAndSetIfChanged(ref _usesExprRaiseSet, value); 129 | } 130 | 131 | /// 132 | /// Gets the observable property. 133 | /// 134 | public string? ObservableProperty => _observableProperty.Value; 135 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/Memory/ReactiveObjectMemoryBenchmark.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Linq; 7 | using System.Reactive.Disposables; 8 | using BenchmarkDotNet.Attributes; 9 | using BenchmarkDotNet.Configs; 10 | using BenchmarkDotNet.Jobs; 11 | using DynamicData.Binding; 12 | using ReactiveMarbles.PropertyChanged; 13 | using ReactiveUI; 14 | 15 | namespace ReactiveMarbles.Mvvm.Benchmarks.Memory; 16 | 17 | /// 18 | /// Benchmark for the reactive object. 19 | /// 20 | [SimpleJob(RuntimeMoniker.Net60)] 21 | [MemoryDiagnoser] 22 | [MarkdownExporterAttribute.GitHub] 23 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 24 | public class ReactiveObjectMemoryBenchmark 25 | { 26 | /// 27 | /// Gets or sets a parameter for the number of creations. 28 | /// 29 | [Params(1, 100, 4000)] 30 | public int CreateNumber { get; set; } 31 | 32 | /// 33 | /// Benchmarks the object creation. 34 | /// 35 | [Benchmark] 36 | [BenchmarkCategory("Memory")] 37 | public void ReactiveObjectCreation() 38 | { 39 | var thing = 40 | Enumerable.Range(0, CreateNumber) 41 | .Select(x => new DummyReactiveObject()) 42 | .ToList(); 43 | } 44 | 45 | /// 46 | /// Benchmarks the object value changes. 47 | /// 48 | [Benchmark] 49 | [BenchmarkCategory("Memory")] 50 | public void ReactiveObjectWithChange() 51 | { 52 | var thing = Enumerable.Range(0, CreateNumber) 53 | .Select(x => new DummyReactiveObject()) 54 | .ToList(); 55 | 56 | foreach (var dummy in thing) 57 | { 58 | dummy.IsNotNullString = "New"; 59 | } 60 | } 61 | 62 | /// 63 | /// A benchmark for reactive object and when any value subscription. 64 | /// 65 | [Benchmark] 66 | [BenchmarkCategory("Memory")] 67 | public void ReactiveObjectWhenAnyValueWithSubscribe() 68 | { 69 | var thing = Enumerable.Range(0, CreateNumber) 70 | .Select(x => new DummyReactiveObject()) 71 | .ToList(); 72 | 73 | foreach (var dummy in thing) 74 | { 75 | dummy.WhenAnyValue(x => x.IsNotNullString).Subscribe(); 76 | } 77 | } 78 | 79 | /// 80 | /// A benchmark for reactive object and when any value subscription and disposal. 81 | /// 82 | [Benchmark] 83 | [BenchmarkCategory("Memory")] 84 | public void ReactiveObjectWhenAnyValueWithSubscribeAndDispose() 85 | { 86 | var disposables = new CompositeDisposable(); 87 | var thing = Enumerable.Range(0, CreateNumber) 88 | .Select(x => new DummyReactiveObject()) 89 | .ToList(); 90 | 91 | foreach (var dummy in thing) 92 | { 93 | dummy.WhenAnyValue(x => x.IsNotNullString).Subscribe().DisposeWith(disposables); 94 | } 95 | } 96 | 97 | /// 98 | /// A benchmark for reactive object and when change subscription. 99 | /// 100 | [Benchmark] 101 | [BenchmarkCategory("Memory")] 102 | public void ReactiveObjectWhenChangedWithSubscribe() 103 | { 104 | var thing = Enumerable.Range(0, CreateNumber) 105 | .Select(x => new DummyReactiveObject()) 106 | .ToList(); 107 | 108 | foreach (var dummy in thing) 109 | { 110 | dummy.WhenChanged(x => x.IsNotNullString).Subscribe(); 111 | } 112 | } 113 | 114 | /// 115 | /// A benchmark for reactive object and when changed subscription. 116 | /// 117 | [Benchmark] 118 | [BenchmarkCategory("Memory")] 119 | public void ReactiveObjectWhenChangedWithSubscribeAndDispose() 120 | { 121 | var disposables = new CompositeDisposable(); 122 | var thing = Enumerable.Range(0, CreateNumber) 123 | .Select(x => new DummyReactiveObject()) 124 | .ToList(); 125 | 126 | foreach (var dummy in thing) 127 | { 128 | dummy.WhenChanged(x => x.IsNotNullString).Subscribe().DisposeWith(disposables); 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/Memory/RxObjectMemoryBenchmark.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Linq; 7 | using System.Reactive.Disposables; 8 | using System.Reactive.Subjects; 9 | using BenchmarkDotNet.Attributes; 10 | using BenchmarkDotNet.Configs; 11 | using BenchmarkDotNet.Jobs; 12 | using ReactiveMarbles.PropertyChanged; 13 | using ReactiveUI; 14 | 15 | namespace ReactiveMarbles.Mvvm.Benchmarks.Memory; 16 | 17 | /// 18 | /// Benchmark for the RxObject. 19 | /// 20 | [SimpleJob(RuntimeMoniker.Net60)] 21 | [MemoryDiagnoser] 22 | [MarkdownExporterAttribute.GitHub] 23 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 24 | public class RxObjectMemoryBenchmark 25 | { 26 | /// 27 | /// Gets or sets a parameter for how many numbers to create. 28 | /// 29 | [Params(1, 100, 4000)] 30 | public int CreateNumber { get; set; } 31 | 32 | /// 33 | /// A benchmark for subject subscription. 34 | /// 35 | [Benchmark(Baseline = true)] 36 | [BenchmarkCategory("Memory")] 37 | public void SubjectWithSubscribe() 38 | { 39 | var thing = Enumerable.Range(0, CreateNumber) 40 | .Select(_ => new Subject()) 41 | .ToList(); 42 | 43 | foreach (var dummy in thing) 44 | { 45 | dummy.Subscribe(); 46 | } 47 | } 48 | 49 | /// 50 | /// A benchmark for the object creation time. 51 | /// 52 | [Benchmark] 53 | [BenchmarkCategory("Memory")] 54 | public void RxObjectCreation() 55 | { 56 | var unused = Enumerable.Range(0, CreateNumber) 57 | .Select(_ => new DummyRxObject()) 58 | .ToList(); 59 | } 60 | 61 | /// 62 | /// A benchmark for the object change time. 63 | /// 64 | [Benchmark] 65 | [BenchmarkCategory("Memory")] 66 | public void RxObjectWithChange() 67 | { 68 | var thing = Enumerable.Range(0, CreateNumber) 69 | .Select(_ => new DummyRxObject()) 70 | .ToList(); 71 | 72 | foreach (var dummy in thing) 73 | { 74 | dummy.IsNotNullString = "New"; 75 | } 76 | } 77 | 78 | /// 79 | /// A benchmark for subject subscription and disposal. 80 | /// 81 | [Benchmark] 82 | [BenchmarkCategory("Memory")] 83 | public void SubjectWithSubscribeAndDispose() 84 | { 85 | var disposables = new CompositeDisposable(); 86 | var thing = Enumerable.Range(0, CreateNumber) 87 | .Select(_ => new Subject()) 88 | .ToList(); 89 | 90 | foreach (var dummy in thing) 91 | { 92 | dummy.Subscribe().DisposeWith(disposables); 93 | } 94 | } 95 | 96 | /// 97 | /// A benchmark for rx object and when any value subscription. 98 | /// 99 | [Benchmark] 100 | [BenchmarkCategory("Memory")] 101 | public void RxObjectWhenAnyValueWithSubscribe() 102 | { 103 | var thing = Enumerable.Range(0, CreateNumber) 104 | .Select(_ => new DummyRxObject()) 105 | .ToList(); 106 | 107 | foreach (var dummy in thing) 108 | { 109 | dummy.WhenAnyValue(x => x.IsNotNullString).Subscribe(); 110 | } 111 | } 112 | 113 | /// 114 | /// A benchmark for rx object and when any value subscription and disposal. 115 | /// 116 | [Benchmark] 117 | [BenchmarkCategory("Memory")] 118 | public void RxObjectWhenAnyValueWithSubscribeAndDispose() 119 | { 120 | var disposables = new CompositeDisposable(); 121 | var thing = Enumerable.Range(0, CreateNumber) 122 | .Select(_ => new DummyRxObject()) 123 | .ToList(); 124 | 125 | foreach (var dummy in thing) 126 | { 127 | dummy.WhenAnyValue(x => x.IsNotNullString).Subscribe().DisposeWith(disposables); 128 | } 129 | } 130 | 131 | /// 132 | /// A benchmark for rx object and when changed subscription. 133 | /// 134 | [Benchmark] 135 | [BenchmarkCategory("Memory")] 136 | public void RxObjectWhenChangedWithSubscribe() 137 | { 138 | var thing = Enumerable.Range(0, CreateNumber) 139 | .Select(_ => new DummyRxObject()) 140 | .ToList(); 141 | 142 | foreach (var dummy in thing) 143 | { 144 | dummy.WhenChanged(x => x.IsNotNullString).Subscribe(); 145 | } 146 | } 147 | 148 | /// 149 | /// A benchmark for rx object and when changed subscription and disposal. 150 | /// 151 | [Benchmark] 152 | [BenchmarkCategory("Memory")] 153 | public void RxObjectWhenChangedWithSubscribeAndDispose() 154 | { 155 | var disposables = new CompositeDisposable(); 156 | var thing = Enumerable.Range(0, CreateNumber) 157 | .Select(_ => new DummyRxObject()) 158 | .ToList(); 159 | 160 | foreach (var dummy in thing) 161 | { 162 | dummy.WhenChanged(x => x.IsNotNullString).Subscribe().DisposeWith(disposables); 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/Performance/AsValuePerformanceBenchmark.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using BenchmarkDotNet.Attributes; 6 | using BenchmarkDotNet.Configs; 7 | using BenchmarkDotNet.Jobs; 8 | 9 | using ReactiveMarbles.Mvvm.Benchmarks.Memory; 10 | using ReactiveMarbles.PropertyChanged; 11 | 12 | namespace ReactiveMarbles.Mvvm.Benchmarks.Performance; 13 | 14 | /// 15 | /// Benchmark for the AsValue. 16 | /// 17 | [SimpleJob(RuntimeMoniker.Net60)] 18 | [MemoryDiagnoser] 19 | [MarkdownExporterAttribute.GitHub] 20 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 21 | public class AsValuePerformanceBenchmark 22 | { 23 | /// 24 | /// Benchmarks AsValue. 25 | /// 26 | [Benchmark(Baseline = true)] 27 | [BenchmarkCategory("Performance")] 28 | public void AsValueBenchmark() 29 | { 30 | DummyRxObject thing = new(); 31 | var unused = thing 32 | .WhenChanged(x => x.NotSerialized, x => x.IsOnlyOneWord, (not, one) => not + one) 33 | .AsValue(onChanged: _ => { }); 34 | } 35 | 36 | /// 37 | /// Benchmarks AsValue when word changes. 38 | /// 39 | [Benchmark] 40 | [BenchmarkCategory("Performance")] 41 | public void AsValueWhenWordChangedBenchmark() 42 | { 43 | _ = new DummyRxObject 44 | { 45 | IsOnlyOneWord = "Two Words", 46 | }; 47 | } 48 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/Performance/ToPropertyPerformanceBenchmark.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using BenchmarkDotNet.Attributes; 6 | using BenchmarkDotNet.Configs; 7 | using BenchmarkDotNet.Jobs; 8 | 9 | using ReactiveMarbles.Mvvm.Benchmarks.Memory; 10 | using ReactiveMarbles.PropertyChanged; 11 | 12 | using ReactiveUI; 13 | 14 | namespace ReactiveMarbles.Mvvm.Benchmarks.Performance; 15 | 16 | /// 17 | /// Benchmark for the ToProperty. 18 | /// 19 | [SimpleJob(RuntimeMoniker.Net60)] 20 | [MemoryDiagnoser] 21 | [MarkdownExporterAttribute.GitHub] 22 | [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] 23 | public class ToPropertyPerformanceBenchmark 24 | { 25 | /// 26 | /// Benchmarks ToProperty performance. 27 | /// 28 | [Benchmark] 29 | [BenchmarkCategory("Performance")] 30 | public void ToPropertyBenchmark() 31 | { 32 | DummyReactiveObject thing = new(); 33 | var unused = 34 | thing 35 | .WhenChanged(x => x.NotSerialized, x => x.IsOnlyOneWord, (not, one) => not + one) 36 | .ToProperty(thing, x => x.ObservableProperty); 37 | } 38 | 39 | /// 40 | /// Benchmarks ToProperty when a word changes. 41 | /// 42 | [Benchmark] 43 | [BenchmarkCategory("Performance")] 44 | public void ToPropertyWhenWordChangedBenchmark() 45 | { 46 | _ = new DummyReactiveObject 47 | { 48 | IsOnlyOneWord = "Two Words", 49 | }; 50 | } 51 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using BenchmarkDotNet.Running; 6 | 7 | namespace ReactiveMarbles.Mvvm.Benchmarks; 8 | 9 | /// 10 | /// Class which hosts the main entry point into the application. 11 | /// 12 | public static class Program 13 | { 14 | /// 15 | /// The main entry point into the benchmarking application. 16 | /// 17 | /// Arguments from the command line. 18 | public static void Main(string[] args) 19 | { 20 | _ = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); 21 | } 22 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/README.MD: -------------------------------------------------------------------------------- 1 | ``` ini 2 | 3 | BenchmarkDotNet=v0.13.1, OS=macOS Monterey 12.1 (21C52) [Darwin 21.2.0] 4 | Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores 5 | .NET SDK=6.0.301 6 | [Host] : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT 7 | .NET 6.0 : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT 8 | 9 | Job=.NET 6.0 Runtime=.NET 6.0 10 | 11 | ``` 12 | 13 | # ReactiveMarbles 14 | 15 | ## RxObject 16 | | Method | CreateNumber | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Gen 2 | Allocated | 17 | |-------------------------------------------- |------------- |-----------------:|----------------:|-----------------:|-------:|--------:|----------:|----------:|----------:|-------------:| 18 | | **SubjectWithSubscribe** | **1** | **163.8 ns** | **0.80 ns** | **0.67 ns** | **1.00** | **0.00** | **0.0353** | **-** | **-** | **296 B** | 19 | | RxObjectCreation | 1 | 15,404.8 ns | 132.73 ns | 124.16 ns | 94.02 | 1.04 | 0.8240 | 0.1221 | - | 7,001 B | 20 | | RxObjectWithChange | 1 | 16,066.7 ns | 125.85 ns | 111.57 ns | 98.04 | 0.84 | 0.8240 | 0.1221 | - | 7,049 B | 21 | | SubjectWithSubscribeAndDispose | 1 | 261.4 ns | 2.52 ns | 2.36 ns | 1.59 | 0.01 | 0.0534 | - | - | 448 B | 22 | | RxObjectWhenAnyValueWithSubscribe | 1 | 34,874.8 ns | 375.44 ns | 332.82 ns | 212.98 | 1.79 | 1.4038 | 0.2441 | - | 12,122 B | 23 | | RxObjectWhenAnyValueWithSubscribeAndDispose | 1 | 35,340.0 ns | 664.53 ns | 711.04 ns | 216.44 | 5.44 | 1.4648 | 0.2441 | - | 12,274 B | 24 | | RxObjectWhenChangedWithSubscribe | 1 | 24,372.1 ns | 152.84 ns | 135.49 ns | 148.72 | 0.98 | 1.1597 | 0.2136 | - | 9,912 B | 25 | | RxObjectWhenChangedWithSubscribeAndDispose | 1 | 24,027.8 ns | 152.85 ns | 135.50 ns | 146.63 | 0.85 | 1.1902 | 0.1831 | - | 10,066 B | 26 | | | | | | | | | | | | | 27 | | **SubjectWithSubscribe** | **100** | **8,490.2 ns** | **22.88 ns** | **19.11 ns** | **1.00** | **0.00** | **1.8311** | **0.0916** | **-** | **15,344 B** | 28 | | RxObjectCreation | 100 | 1,601,101.5 ns | 7,304.69 ns | 6,099.75 ns | 188.58 | 0.89 | 80.0781 | 31.2500 | - | 685,594 B | 29 | | RxObjectWithChange | 100 | 1,579,546.5 ns | 11,572.24 ns | 10,824.68 ns | 185.91 | 1.28 | 82.0313 | 35.1563 | - | 690,462 B | 30 | | SubjectWithSubscribeAndDispose | 100 | 12,877.0 ns | 30.04 ns | 25.08 ns | 1.52 | 0.01 | 2.0905 | 0.1068 | - | 17,600 B | 31 | | RxObjectWhenAnyValueWithSubscribe | 100 | 2,942,318.0 ns | 25,730.88 ns | 24,068.68 ns | 346.52 | 3.47 | 140.6250 | 66.4063 | - | 1,197,315 B | 32 | | RxObjectWhenAnyValueWithSubscribeAndDispose | 100 | 3,043,304.2 ns | 16,970.28 ns | 15,874.01 ns | 358.35 | 2.17 | 140.6250 | 70.3125 | - | 1,199,888 B | 33 | | RxObjectWhenChangedWithSubscribe | 100 | 2,412,206.3 ns | 20,327.44 ns | 19,014.30 ns | 284.45 | 2.21 | 113.2813 | 42.9688 | - | 976,616 B | 34 | | RxObjectWhenChangedWithSubscribeAndDispose | 100 | 2,293,934.2 ns | 13,820.20 ns | 12,927.42 ns | 270.23 | 1.50 | 113.2813 | 54.6875 | - | 978,872 B | 35 | | | | | | | | | | | | | 36 | | **SubjectWithSubscribe** | **4000** | **395,955.1 ns** | **2,439.15 ns** | **2,162.24 ns** | **1.00** | **0.00** | **72.2656** | **31.7383** | **-** | **608,144 B** | 37 | | RxObjectCreation | 4000 | 121,556,272.3 ns | 1,393,992.12 ns | 1,303,941.09 ns | 306.69 | 4.38 | 3600.0000 | 1400.0000 | 400.0000 | 27,429,170 B | 38 | | RxObjectWithChange | 4000 | 133,998,168.1 ns | 2,663,042.36 ns | 6,172,005.62 ns | 331.52 | 19.73 | 3750.0000 | 1500.0000 | 500.0000 | 27,622,806 B | 39 | | SubjectWithSubscribeAndDispose | 4000 | 593,221.8 ns | 11,081.81 ns | 11,380.20 ns | 1.50 | 0.03 | 80.0781 | 33.2031 | - | 674,009 B | 40 | | RxObjectWhenAnyValueWithSubscribe | 4000 | 249,498,696.7 ns | 4,984,701.60 ns | 3,891,727.13 ns | 630.13 | 11.25 | 6500.0000 | 3000.0000 | 1000.0000 | 47,911,968 B | 41 | | RxObjectWhenAnyValueWithSubscribeAndDispose | 4000 | 246,374,019.5 ns | 4,739,604.23 ns | 12,149,417.59 ns | 621.43 | 28.42 | 6500.0000 | 3000.0000 | 1000.0000 | 47,976,152 B | 42 | | RxObjectWhenChangedWithSubscribe | 4000 | 136,864,129.7 ns | 2,564,240.52 ns | 2,398,592.17 ns | 344.97 | 6.14 | 4000.0000 | 1000.0000 | - | 39,026,848 B | 43 | | RxObjectWhenChangedWithSubscribeAndDispose | 4000 | 139,549,864.6 ns | 1,878,092.90 ns | 1,664,880.44 ns | 352.44 | 4.24 | 4000.0000 | 1000.0000 | - | 39,173,744 B | 44 | 45 | ## AsValue 46 | 47 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated | 48 | |----------------------------------- |----------:|---------:|---------:|------:|--------:|-------:|-------:|----------:| 49 | | AsValueBenchmark | 48.78 μs | 0.257 μs | 0.241 μs | 1.00 | 0.00 | 1.8311 | 0.3662 | 15 KB | 50 | | AsValueWhenWordChangedBenchmark | 20.54 μs | 0.096 μs | 0.090 μs | 0.42 | 0.00 | 0.7935 | 0.1221 | 7 KB | 51 | 52 | 53 | # ReactiveUI 54 | 55 | ## ReactiveObject 56 | | Method | CreateNumber | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | 57 | |-------------------------------------------------- |------------- |--------------:|-------------:|--------------:|--------------:|----------:|----------:|----------:|----------:| 58 | | **ReactiveObjectCreation** | **1** | **35.28 μs** | **0.628 μs** | **0.838 μs** | **35.01 μs** | **1.5259** | **0.1831** | **0.0610** | **12 KB** | 59 | | ReactiveObjectWithChange | 1 | 35.50 μs | 0.639 μs | 0.534 μs | 35.63 μs | 1.5259 | 0.1831 | 0.0610 | 13 KB | 60 | | ReactiveObjectWhenAnyValueWithSubscribe | 1 | 47.36 μs | 0.918 μs | 1.057 μs | 46.83 μs | 2.0142 | 0.3662 | 0.1221 | 16 KB | 61 | | ReactiveObjectWhenAnyValueWithSubscribeAndDispose | 1 | 46.78 μs | 0.342 μs | 0.320 μs | 46.78 μs | 2.0142 | 0.3662 | 0.1221 | 17 KB | 62 | | ReactiveObjectWhenChangedWithSubscribe | 1 | 55.65 μs | 0.338 μs | 0.316 μs | 55.68 μs | 2.0752 | 0.3662 | 0.1221 | 17 KB | 63 | | ReactiveObjectWhenChangedWithSubscribeAndDispose | 1 | 54.58 μs | 0.340 μs | 0.318 μs | 54.66 μs | 2.1362 | 0.3662 | 0.1221 | 18 KB | 64 | | **ReactiveObjectCreation** | **100** | **3,734.45 μs** | **29.458 μs** | **24.599 μs** | **3,734.25 μs** | **148.4375** | **70.3125** | **15.6250** | **1,234 KB** | 65 | | ReactiveObjectWithChange | 100 | 3,796.80 μs | 75.653 μs | 119.994 μs | 3,776.35 μs | 152.3438 | 66.4063 | 15.6250 | 1,266 KB | 66 | | ReactiveObjectWhenAnyValueWithSubscribe | 100 | 5,070.60 μs | 99.147 μs | 286.062 μs | 4,988.22 μs | 195.3125 | 78.1250 | 15.6250 | 1,633 KB | 67 | | ReactiveObjectWhenAnyValueWithSubscribeAndDispose | 100 | 5,230.51 μs | 72.298 μs | 64.090 μs | 5,230.02 μs | 195.3125 | 93.7500 | 39.0625 | 1,637 KB | 68 | | ReactiveObjectWhenChangedWithSubscribe | 100 | 5,291.38 μs | 87.529 μs | 77.592 μs | 5,306.46 μs | 210.9375 | 101.5625 | 39.0625 | 1,723 KB | 69 | | ReactiveObjectWhenChangedWithSubscribeAndDispose | 100 | 5,160.44 μs | 39.409 μs | 32.908 μs | 5,153.08 μs | 210.9375 | 101.5625 | 31.2500 | 1,726 KB | 70 | | **ReactiveObjectCreation** | **4000** | **274,614.27 μs** | **3,976.521 μs** | **4,419.892 μs** | **272,998.57 μs** | **7000.0000** | **3000.0000** | **1000.0000** | **49,307 KB** | 71 | | ReactiveObjectWithChange | 4000 | 262,808.06 μs | 5,248.733 μs | 5,154.958 μs | 262,669.73 μs | 7000.0000 | 3000.0000 | 1000.0000 | 50,742 KB | 72 | | ReactiveObjectWhenAnyValueWithSubscribe | 4000 | 371,405.46 μs | 7,269.827 μs | 12,922.110 μs | 368,357.67 μs | 9000.0000 | 4000.0000 | 1000.0000 | 65,274 KB | 73 | | ReactiveObjectWhenAnyValueWithSubscribeAndDispose | 4000 | 342,062.91 μs | 8,536.027 μs | 25,168.668 μs | 344,686.07 μs | 9000.0000 | 4000.0000 | 1000.0000 | 65,490 KB | 74 | | ReactiveObjectWhenChangedWithSubscribe | 4000 | 371,596.61 μs | 7,155.489 μs | 10,927.188 μs | 375,727.41 μs | 9000.0000 | 4000.0000 | 1000.0000 | 68,876 KB | 75 | | ReactiveObjectWhenChangedWithSubscribeAndDispose | 4000 | 369,832.81 μs | 7,322.658 μs | 10,501.937 μs | 374,104.44 μs | 9000.0000 | 4000.0000 | 1000.0000 | 68,941 KB | 76 | 77 | 78 | ## ToProperty 79 | | Method | Mean | Error | StdDev | Ratio | RatioSD | Gen 0 | Gen 1 | Allocated | 80 | |----------------------------------- |----------:|---------:|---------:|------:|--------:|-------:|-------:|----------:| 81 | | ToPropertyBenchmark | 105.46 μs | 0.971 μs | 0.860 μs | 2.16 | 0.02 | 2.9297 | 0.4883 | 25 KB | 82 | | ToPropertyWhenWordChangedBenchmark | 54.09 μs | 1.078 μs | 1.580 μs | 1.12 | 0.03 | 1.5869 | 0.2441 | 13 KB | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/ReactiveMarbles.Mvvm.Benchmarks.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | Exe 7 | false 8 | $(NoWarn);SA1309;SA1101 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Benchmarks/directory.build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/AsLazyValueExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using FluentAssertions; 6 | 7 | using Microsoft.Reactive.Testing; 8 | 9 | using ReactiveMarbles.PropertyChanged; 10 | 11 | using Xunit; 12 | 13 | namespace ReactiveMarbles.Mvvm.Tests; 14 | 15 | /// 16 | /// Tests for the . 17 | /// 18 | public class AsLazyValueExtensionsTests 19 | { 20 | /// 21 | /// Tests the default value. 22 | /// 23 | [Fact] 24 | public void GivenNoChanges_WhenAsValue_ThenFullNameIsEmpty() 25 | { 26 | // Given, When 27 | AsLazyValueTestObject? sut = new(); 28 | 29 | // Then 30 | _ = sut.FullName.Should().BeNullOrEmpty(); 31 | } 32 | 33 | /// 34 | /// Tests the property is produced from the sequence. 35 | /// 36 | /// The first name. 37 | /// The last name. 38 | [Theory] 39 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 40 | public void GivenSequence_WhenAsValue_ThenPropertyProduced(string first, string last) 41 | { 42 | // Given 43 | AsLazyValueTestObject? sut = new() 44 | { 45 | // When 46 | FirstName = first, 47 | LastName = last, 48 | }; 49 | 50 | // Then 51 | _ = sut.FullName.Should().Be(first + last); 52 | } 53 | 54 | /// 55 | /// Tests the property is produced from the sequence. 56 | /// 57 | /// The first name. 58 | /// The last name. 59 | [Theory] 60 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 61 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1163:Unused parameter.", Justification = "Deliberate")] 62 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "Deliberate")] 63 | public void GivenFirstName_WhenAsValue_ThenPropertyProduced(string first, string last) 64 | { 65 | // Given 66 | AsLazyValueTestObject? sut = new() 67 | { 68 | // When 69 | FirstName = first, 70 | }; 71 | 72 | // Then 73 | _ = sut.FullName.Should().Be(first); 74 | } 75 | 76 | /// 77 | /// Tests the property is produced from the sequence. 78 | /// 79 | /// The first name. 80 | /// The last name. 81 | [Theory] 82 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 83 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1163:Unused parameter.", Justification = "Deliberate")] 84 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "Deliberate")] 85 | public void GivenLastName_WhenAsValue_ThenPropertyProduced(string first, string last) 86 | { 87 | // Given 88 | AsLazyValueTestObject? sut = new() 89 | { 90 | // When 91 | LastName = last, 92 | }; 93 | 94 | // Then 95 | _ = sut.FullName.Should().Be(last); 96 | } 97 | 98 | /// 99 | /// Tests the value of the value binder. 100 | /// 101 | /// The first name. 102 | /// The last name. 103 | [Theory] 104 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 105 | public void GivenOnChanged_WhenAsValue_ThenValueCorrect(string first, string last) 106 | { 107 | // Given 108 | AsLazyValueTestObject? testObject = new(); 109 | var sut = 110 | testObject 111 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 112 | .AsLazyValue(onChanged: _ => { }); 113 | 114 | // When 115 | testObject.FirstName = first; 116 | testObject.LastName = last; 117 | 118 | // Then 119 | _ = sut.Value.Should().Be(first + last); 120 | } 121 | 122 | /// 123 | /// Tests the value of the value binder. 124 | /// 125 | /// The first name. 126 | /// The last name. 127 | [Theory] 128 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 129 | public void GivenOnChangedAndInitialValue_WhenAsValue_ThenValueCorrect(string first, string last) 130 | { 131 | // Given 132 | AsLazyValueTestObject? testObject = new(); 133 | var sut = 134 | testObject 135 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 136 | .AsLazyValue(onChanged: _ => { }, initialValue: () => string.Empty); 137 | 138 | // When 139 | testObject.FirstName = first; 140 | testObject.LastName = last; 141 | 142 | // Then 143 | _ = sut.Value.Should().Be(first + last); 144 | } 145 | 146 | /// 147 | /// Tests the value of the value binder. 148 | /// 149 | /// The first name. 150 | /// The last name. 151 | [Theory] 152 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 153 | public void GivenOnChangedAndOnChangingAndInitialValue_WhenAsValue_ThenValueCorrect(string first, string last) 154 | { 155 | // Given 156 | AsLazyValueTestObject? testObject = new(); 157 | var sut = 158 | testObject 159 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 160 | .AsLazyValue(onChanging: _ => { }, onChanged: _ => { }, initialValue: () => string.Empty); 161 | 162 | // When 163 | testObject.FirstName = first; 164 | testObject.LastName = last; 165 | 166 | // Then 167 | _ = sut.Value.Should().Be(first + last); 168 | } 169 | 170 | /// 171 | /// Tests the value of the value binder. 172 | /// 173 | /// The first name. 174 | /// The last name. 175 | [Theory] 176 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 177 | public void GivenOnChangedAndOnChangingAndSchedulerAndInitialValue_WhenAsValue_ThenValueCorrect(string first, string last) 178 | { 179 | // Given 180 | const string start = "start"; 181 | TestScheduler? testScheduler = new(); 182 | AsLazyValueTestObject? testObject = new(); 183 | var sut = 184 | testObject 185 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 186 | .AsLazyValue(onChanged: _ => { }, testScheduler, () => start); 187 | 188 | _ = sut.Value.Should().Be(start); 189 | 190 | // When 191 | testObject.FirstName = first; 192 | testObject.LastName = last; 193 | testScheduler.Start(); 194 | 195 | // Then 196 | _ = sut.Value.Should().Be(first + last); 197 | } 198 | 199 | /// 200 | /// Tests the value of the value binder. 201 | /// 202 | /// The first name. 203 | /// The last name. 204 | [Theory] 205 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 206 | public void GivenAllParameters_WhenAsValue_ThenValueCorrect(string first, string last) 207 | { 208 | // Given 209 | const string start = "start"; 210 | TestScheduler? testScheduler = new(); 211 | AsLazyValueTestObject? testObject = new(); 212 | var sut = 213 | testObject 214 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 215 | .AsLazyValue(_ => { }, _ => { }, testScheduler, () => start); 216 | 217 | _ = sut.Value.Should().Be(start); 218 | 219 | // When 220 | testObject.FirstName = first; 221 | testObject.LastName = last; 222 | testScheduler.Start(); 223 | 224 | // Then 225 | _ = sut.Value.Should().Be(first + last); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/AsLazyValueTestObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using ReactiveMarbles.PropertyChanged; 6 | 7 | namespace ReactiveMarbles.Mvvm.Tests; 8 | 9 | /// 10 | /// Test the value thing. 11 | /// 12 | public class AsLazyValueTestObject : RxObject 13 | { 14 | private readonly IValueBinder _fullName; 15 | private string _firstName = string.Empty; 16 | private string _lastName = string.Empty; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public AsLazyValueTestObject() 22 | { 23 | _fullName = 24 | this.WhenChanged( 25 | x => x.FirstName, 26 | x => x.LastName, 27 | (first, last) => first + last) 28 | .AsLazyValue(onChanged: _ => RaisePropertyChanged(nameof(FullName))); 29 | } 30 | 31 | /// 32 | /// Gets or sets the first name. 33 | /// 34 | public string FirstName 35 | { 36 | get => _firstName; 37 | set => RaiseAndSetIfChanged(ref _firstName, value); 38 | } 39 | 40 | /// 41 | /// Gets or sets the last name. 42 | /// 43 | public string LastName 44 | { 45 | get => _lastName; 46 | set => RaiseAndSetIfChanged(ref _lastName, value); 47 | } 48 | 49 | /// 50 | /// Gets the full name. 51 | /// 52 | public string? FullName => _fullName.Value; 53 | } 54 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/AsValueExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using FluentAssertions; 6 | 7 | using Microsoft.Reactive.Testing; 8 | 9 | using ReactiveMarbles.PropertyChanged; 10 | 11 | using Xunit; 12 | 13 | namespace ReactiveMarbles.Mvvm.Tests; 14 | 15 | /// 16 | /// Tests for the . 17 | /// 18 | public class AsValueExtensionsTests 19 | { 20 | /// 21 | /// Tests the default value. 22 | /// 23 | [Fact] 24 | public void GivenNoChanges_WhenAsValue_ThenFullNameIsEmpty() 25 | { 26 | // Given, When 27 | AsValueTestObject? sut = new(); 28 | 29 | // Then 30 | _ = sut.FullName.Should().BeNullOrEmpty(); 31 | } 32 | 33 | /// 34 | /// Tests the property is produced from the sequence. 35 | /// 36 | /// The first name. 37 | /// The last name. 38 | [Theory] 39 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 40 | public void GivenSequence_WhenAsValue_ThenPropertyProduced(string first, string last) 41 | { 42 | // Given 43 | AsValueTestObject? sut = new() 44 | { 45 | // When 46 | FirstName = first, 47 | LastName = last, 48 | }; 49 | 50 | // Then 51 | _ = sut.FullName.Should().Be(first + last); 52 | } 53 | 54 | /// 55 | /// Tests the property is produced from the sequence. 56 | /// 57 | /// The first name. 58 | /// The last name. 59 | [Theory] 60 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 61 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1163:Unused parameter.", Justification = "Deliberate")] 62 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "Deliberate")] 63 | public void GivenFirstName_WhenAsValue_ThenPropertyProduced(string first, string last) 64 | { 65 | // Given 66 | AsValueTestObject? sut = new() 67 | { 68 | // When 69 | FirstName = first, 70 | }; 71 | 72 | // Then 73 | _ = sut.FullName.Should().Be(first); 74 | } 75 | 76 | /// 77 | /// Tests the property is produced from the sequence. 78 | /// 79 | /// The first name. 80 | /// The last name. 81 | [Theory] 82 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 83 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Roslynator", "RCS1163:Unused parameter.", Justification = "Deliberate")] 84 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "xUnit1026:Theory methods should use all of their parameters", Justification = "Deliberate")] 85 | public void GivenLastName_WhenAsValue_ThenPropertyProduced(string first, string last) 86 | { 87 | // Given 88 | AsValueTestObject? sut = new() 89 | { 90 | // When 91 | LastName = last, 92 | }; 93 | 94 | // Then 95 | _ = sut.FullName.Should().Be(last); 96 | } 97 | 98 | /// 99 | /// Tests the value of the value binder. 100 | /// 101 | /// The first name. 102 | /// The last name. 103 | [Theory] 104 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 105 | public void GivenOnChanged_WhenAsValue_ThenValueCorrect(string first, string last) 106 | { 107 | // Given 108 | AsValueTestObject? testObject = new(); 109 | var sut = 110 | testObject 111 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 112 | .AsValue(onChanged: _ => { }); 113 | 114 | // When 115 | testObject.FirstName = first; 116 | testObject.LastName = last; 117 | 118 | // Then 119 | _ = sut.Value.Should().Be(first + last); 120 | } 121 | 122 | /// 123 | /// Tests the value of the value binder. 124 | /// 125 | /// The first name. 126 | /// The last name. 127 | [Theory] 128 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 129 | public void GivenOnChangedAndInitialValue_WhenAsValue_ThenValueCorrect(string first, string last) 130 | { 131 | // Given 132 | AsValueTestObject? testObject = new(); 133 | var sut = 134 | testObject 135 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 136 | .AsValue(onChanged: _ => { }, initialValue: () => string.Empty); 137 | 138 | // When 139 | testObject.FirstName = first; 140 | testObject.LastName = last; 141 | 142 | // Then 143 | _ = sut.Value.Should().Be(first + last); 144 | } 145 | 146 | /// 147 | /// Tests the value of the value binder. 148 | /// 149 | /// The first name. 150 | /// The last name. 151 | [Theory] 152 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 153 | public void GivenOnChangedAndOnChangingAndInitialValue_WhenAsValue_ThenValueCorrect(string first, string last) 154 | { 155 | // Given 156 | AsValueTestObject? testObject = new(); 157 | var sut = 158 | testObject 159 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 160 | .AsValue(onChanging: _ => { }, onChanged: _ => { }, initialValue: () => string.Empty); 161 | 162 | // When 163 | testObject.FirstName = first; 164 | testObject.LastName = last; 165 | 166 | // Then 167 | _ = sut.Value.Should().Be(first + last); 168 | } 169 | 170 | /// 171 | /// Tests the value of the value binder. 172 | /// 173 | /// The first name. 174 | /// The last name. 175 | [Theory] 176 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 177 | public void GivenOnChangedAndOnChangingAndSchedulerAndInitialValue_WhenAsValue_ThenValueCorrect(string first, string last) 178 | { 179 | // Given 180 | TestScheduler? scheduler = new(); 181 | AsValueTestObject? testObject = new(); 182 | var sut = 183 | testObject 184 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 185 | .AsValue(onChanged: _ => { }, scheduler, initialValue: () => string.Empty); 186 | 187 | // When 188 | testObject.FirstName = first; 189 | testObject.LastName = last; 190 | scheduler.Start(); 191 | 192 | // Then 193 | _ = sut.Value.Should().Be(first + last); 194 | } 195 | 196 | /// 197 | /// Tests the value of the value binder. 198 | /// 199 | /// The first name. 200 | /// The last name. 201 | [Theory] 202 | [MemberData(nameof(AsValueTestData.Data), MemberType = typeof(AsValueTestData))] 203 | public void GivenAllParameters_WhenAsValue_ThenValueCorrect(string first, string last) 204 | { 205 | // Given 206 | TestScheduler? scheduler = new(); 207 | AsValueTestObject? testObject = new(); 208 | var sut = 209 | testObject 210 | .WhenChanged(x => x.FirstName, x => x.LastName, (firstName, lastName) => firstName + lastName) 211 | .AsValue(_ => { }, _ => { }, scheduler, () => string.Empty); 212 | 213 | // When 214 | testObject.FirstName = first; 215 | testObject.LastName = last; 216 | scheduler.Start(); 217 | 218 | // Then 219 | _ = sut.Value.Should().Be(first + last); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/AsValueTestData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace ReactiveMarbles.Mvvm.Tests; 8 | 9 | /// 10 | /// Represents test data for As Value Extension tests. 11 | /// 12 | public static class AsValueTestData 13 | { 14 | /// 15 | /// Gets test data. 16 | /// 17 | public static IEnumerable Data => 18 | [ 19 | ["Leeroy", "Jenkins"], 20 | ["James", "Kirk"], 21 | ["Major", "Payne"], 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/AsValueTestObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using ReactiveMarbles.PropertyChanged; 6 | 7 | namespace ReactiveMarbles.Mvvm.Tests; 8 | 9 | /// 10 | /// Test the value thing. 11 | /// 12 | public class AsValueTestObject : RxObject 13 | { 14 | private readonly IValueBinder _fullName; 15 | private string _firstName = string.Empty; 16 | private string _lastName = string.Empty; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | public AsValueTestObject() 22 | { 23 | _fullName = 24 | this.WhenChanged( 25 | x => x.FirstName, 26 | x => x.LastName, 27 | (first, last) => first + last) 28 | .AsValue(onChanged: _ => RaisePropertyChanged(nameof(FullName))); 29 | } 30 | 31 | /// 32 | /// Gets or sets the first name. 33 | /// 34 | public string FirstName 35 | { 36 | get => _firstName; 37 | set => RaiseAndSetIfChanged(ref _firstName, value); 38 | } 39 | 40 | /// 41 | /// Gets or sets the last name. 42 | /// 43 | public string LastName 44 | { 45 | get => _lastName; 46 | set => RaiseAndSetIfChanged(ref _lastName, value); 47 | } 48 | 49 | /// 50 | /// Gets the full name. 51 | /// 52 | public string? FullName => _fullName.Value; 53 | } 54 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ReactiveMarbles.Mvvm.Tests.Properties { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class Resources { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Resources() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ReactiveMarbles.Mvvm.Tests.Properties.Resources", typeof(Resources).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Oops!? {0} is required.. 65 | /// 66 | public static string ValidationErrorMessage { 67 | get { 68 | return ResourceManager.GetString("ValidationErrorMessage", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to FromResource. 74 | /// 75 | public static string ValidationTargetPropertyName { 76 | get { 77 | return ResourceManager.GetString("ValidationTargetPropertyName", resourceCulture); 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Oops!? {0} is required. 122 | 123 | 124 | FromResource 125 | 126 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/ReactiveMarbles.Mvvm.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | preview 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | True 37 | True 38 | Resources.resx 39 | 40 | 41 | 42 | 43 | 44 | PublicResXFileCodeGenerator 45 | Resources.Designer.cs 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/ReactiveProperty/Mocks/ReactivePropertyVM.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Threading.Tasks; 7 | using ReactiveMarbles.Mvvm.Tests.Properties; 8 | 9 | namespace ReactiveMarbles.Mvvm.Tests.ReactiveProperty.Mocks 10 | { 11 | internal class ReactivePropertyVM : RxObject 12 | { 13 | public ReactivePropertyVM() 14 | { 15 | IsRequiredProperty = new ReactiveProperty() 16 | .AddValidation(() => IsRequiredProperty); 17 | 18 | LengthLessThanFiveProperty = new ReactiveProperty() 19 | .AddValidation(() => LengthLessThanFiveProperty) 20 | .AddValidationError(s => string.IsNullOrWhiteSpace(s) ? "required" : null); 21 | 22 | TaskValidationTestProperty = new ReactiveProperty() 23 | .AddValidationError(async s => await Task.FromResult(string.IsNullOrWhiteSpace(s) ? "required" : default)); 24 | 25 | CustomValidationErrorMessageProperty = new ReactiveProperty() 26 | .AddValidation(() => CustomValidationErrorMessageProperty); 27 | 28 | CustomValidationErrorMessageWithDisplayNameProperty = new ReactiveProperty() 29 | .AddValidation(() => CustomValidationErrorMessageWithDisplayNameProperty); 30 | 31 | CustomValidationErrorMessageWithResourceProperty = new ReactiveProperty() 32 | .AddValidation(() => CustomValidationErrorMessageWithResourceProperty); 33 | } 34 | 35 | [Required(ErrorMessage = "error!")] 36 | public ReactiveProperty IsRequiredProperty { get; } 37 | 38 | [StringLength(5, ErrorMessage = "5over")] 39 | public ReactiveProperty LengthLessThanFiveProperty { get; } 40 | 41 | public ReactiveProperty TaskValidationTestProperty { get; } 42 | 43 | [Required(ErrorMessage = "Custom validation error message for {0}")] 44 | public ReactiveProperty CustomValidationErrorMessageProperty { get; } 45 | 46 | [Required(ErrorMessage = "Custom validation error message for {0}")] 47 | [Display(Name = "CustomName")] 48 | public ReactiveProperty CustomValidationErrorMessageWithDisplayNameProperty { get; } 49 | 50 | [Required(ErrorMessageResourceType = typeof(Resources), ErrorMessageResourceName = nameof(Resources.ValidationErrorMessage))] 51 | [Display(ResourceType = typeof(Resources), Name = nameof(Resources.ValidationTargetPropertyName))] 52 | public ReactiveProperty CustomValidationErrorMessageWithResourceProperty { get; } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/ReactiveProperty/TestEnum.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveUI.Tests.ReactiveProperty 6 | { 7 | internal enum TestEnum 8 | { 9 | /// 10 | /// The none. 11 | /// 12 | None, 13 | 14 | /// 15 | /// The enum1. 16 | /// 17 | Enum1, 18 | 19 | /// 20 | /// The enum2. 21 | /// 22 | Enum2, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/RxObjectTestFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Runtime.Serialization; 8 | 9 | using DynamicData.Binding; 10 | 11 | namespace ReactiveMarbles.Mvvm.Tests; 12 | 13 | /// 14 | /// A test fixture. 15 | /// 16 | /// 17 | [DataContract] 18 | public class RxObjectTestFixture : RxObject 19 | { 20 | [IgnoreDataMember] 21 | private string? _isNotNullString; 22 | 23 | [IgnoreDataMember] 24 | private string? _isOnlyOneWord; 25 | 26 | private string? _notSerialized; 27 | 28 | [IgnoreDataMember] 29 | private int? _nullableInt; 30 | 31 | [IgnoreDataMember] 32 | private string? _pocoProperty; 33 | 34 | [IgnoreDataMember] 35 | private List? _stackOverflowTrigger; 36 | 37 | [IgnoreDataMember] 38 | private string? _usesExprRaiseSet; 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// 43 | public RxObjectTestFixture() 44 | { 45 | TestCollection = []; 46 | } 47 | 48 | /// 49 | /// Gets or sets the is not null string. 50 | /// 51 | [DataMember] 52 | public string? IsNotNullString 53 | { 54 | get => _isNotNullString; 55 | set => RaiseAndSetIfChanged(ref _isNotNullString, value); 56 | } 57 | 58 | /// 59 | /// Gets or sets the is only one word. 60 | /// 61 | [DataMember] 62 | public string? IsOnlyOneWord 63 | { 64 | get => _isOnlyOneWord; 65 | set => RaiseAndSetIfChanged(ref _isOnlyOneWord, value); 66 | } 67 | 68 | /// 69 | /// Gets or sets the not serialized. 70 | /// 71 | public string? NotSerialized 72 | { 73 | get => _notSerialized; 74 | set => RaiseAndSetIfChanged(ref _notSerialized, value); 75 | } 76 | 77 | /// 78 | /// Gets or sets the nullable int. 79 | /// 80 | [DataMember] 81 | public int? NullableInt 82 | { 83 | get => _nullableInt; 84 | set => RaiseAndSetIfChanged(ref _nullableInt, value); 85 | } 86 | 87 | /// 88 | /// Gets or sets the poco property. 89 | /// 90 | [DataMember] 91 | public string? PocoProperty 92 | { 93 | get => _pocoProperty; 94 | set => _pocoProperty = value; 95 | } 96 | 97 | /// 98 | /// Gets or sets the stack overflow trigger. 99 | /// 100 | [DataMember] 101 | public List? StackOverflowTrigger 102 | { 103 | get => _stackOverflowTrigger; 104 | set => RaiseAndSetIfChanged(ref _stackOverflowTrigger, value?.ToList()); 105 | } 106 | 107 | /// 108 | /// Gets or sets the test collection. 109 | /// 110 | [DataMember] 111 | public ObservableCollectionExtended TestCollection { get; protected set; } 112 | 113 | /// 114 | /// Gets or sets the uses expr raise set. 115 | /// 116 | [DataMember] 117 | public string? UsesExprRaiseSet 118 | { 119 | get => _usesExprRaiseSet; 120 | set => RaiseAndSetIfChanged(ref _usesExprRaiseSet, value); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/RxRecordTestFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Runtime.Serialization; 8 | 9 | using DynamicData.Binding; 10 | 11 | namespace ReactiveMarbles.Mvvm.Tests; 12 | 13 | /// 14 | /// A test fixture. 15 | /// 16 | /// 17 | [DataContract] 18 | public record RxRecordTestFixture : RxRecord 19 | { 20 | [IgnoreDataMember] 21 | private string? _isNotNullString; 22 | 23 | [IgnoreDataMember] 24 | private string? _isOnlyOneWord; 25 | 26 | private string? _notSerialized; 27 | 28 | [IgnoreDataMember] 29 | private int? _nullableInt; 30 | 31 | [IgnoreDataMember] 32 | private string? _pocoProperty; 33 | 34 | [IgnoreDataMember] 35 | private List? _stackOverflowTrigger; 36 | 37 | [IgnoreDataMember] 38 | private string? _usesExprRaiseSet; 39 | 40 | /// 41 | /// Initializes a new instance of the class. 42 | /// 43 | public RxRecordTestFixture() 44 | { 45 | TestCollection = []; 46 | } 47 | 48 | /// 49 | /// Gets or sets the is not null string. 50 | /// 51 | [DataMember] 52 | public string? IsNotNullString 53 | { 54 | get => _isNotNullString; 55 | set => RaiseAndSetIfChanged(ref _isNotNullString, value); 56 | } 57 | 58 | /// 59 | /// Gets or sets the is only one word. 60 | /// 61 | [DataMember] 62 | public string? IsOnlyOneWord 63 | { 64 | get => _isOnlyOneWord; 65 | set => RaiseAndSetIfChanged(ref _isOnlyOneWord, value); 66 | } 67 | 68 | /// 69 | /// Gets or sets the not serialized. 70 | /// 71 | public string? NotSerialized 72 | { 73 | get => _notSerialized; 74 | set => RaiseAndSetIfChanged(ref _notSerialized, value); 75 | } 76 | 77 | /// 78 | /// Gets or sets the nullable int. 79 | /// 80 | [DataMember] 81 | public int? NullableInt 82 | { 83 | get => _nullableInt; 84 | set => RaiseAndSetIfChanged(ref _nullableInt, value); 85 | } 86 | 87 | /// 88 | /// Gets or sets the poco property. 89 | /// 90 | [DataMember] 91 | public string? PocoProperty 92 | { 93 | get => _pocoProperty; 94 | set => _pocoProperty = value; 95 | } 96 | 97 | /// 98 | /// Gets or sets the stack overflow trigger. 99 | /// 100 | [DataMember] 101 | public List? StackOverflowTrigger 102 | { 103 | get => _stackOverflowTrigger; 104 | set => RaiseAndSetIfChanged(ref _stackOverflowTrigger, value?.ToList()); 105 | } 106 | 107 | /// 108 | /// Gets or sets the test collection. 109 | /// 110 | [DataMember] 111 | public ObservableCollectionExtended TestCollection { get; protected set; } 112 | 113 | /// 114 | /// Gets or sets the uses expr raise set. 115 | /// 116 | [DataMember] 117 | public string? UsesExprRaiseSet 118 | { 119 | get => _usesExprRaiseSet; 120 | set => RaiseAndSetIfChanged(ref _usesExprRaiseSet, value); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/ScheduledSubjectTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive; 7 | 8 | using FluentAssertions; 9 | 10 | using Microsoft.Reactive.Testing; 11 | 12 | using Xunit; 13 | 14 | namespace ReactiveMarbles.Mvvm.Tests; 15 | 16 | /// 17 | /// ScheduledSubject Tests. 18 | /// 19 | public class ScheduledSubjectTests 20 | { 21 | /// 22 | /// Tests ScheduledSubject. 23 | /// 24 | [Fact] 25 | public void GivenTestScheduler_WhenSubscribed_ThenReturnResult() 26 | { 27 | // Given 28 | Unit? result = null; 29 | TestScheduler? scheduler = new(); 30 | ProxyScheduledSubject? sut = new(scheduler); 31 | _ = sut.Subscribe(actual => result = actual); 32 | 33 | // When 34 | sut.OnNext(Unit.Default); 35 | scheduler.Start(); 36 | 37 | // Then 38 | _ = result.HasValue.Should().BeTrue(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/TestObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.Mvvm.Tests; 6 | 7 | /// 8 | /// Test the thing. 9 | /// 10 | public class TestObject : RxObject 11 | { 12 | private string _testProperty = string.Empty; 13 | 14 | /// 15 | /// Gets or sets test propety. 16 | /// 17 | public string TestProperty 18 | { 19 | get => _testProperty; 20 | set => RaiseAndSetIfChanged(ref _testProperty, value); 21 | } 22 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.Tests/TestSequencer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace ReactiveMarbles.Mvvm.Testing; 10 | 11 | /// 12 | /// Test Sequencer. 13 | /// 14 | /// 15 | public class TestSequencer : IDisposable 16 | { 17 | private readonly Barrier _phaseSync; 18 | private bool _disposedValue; 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | public TestSequencer() => _phaseSync = new(2); 24 | 25 | /// 26 | /// Gets the number of completed phases. 27 | /// 28 | /// 29 | /// The completed phases. 30 | /// 31 | public long CompletedPhases => _phaseSync.CurrentPhaseNumber; 32 | 33 | /// 34 | /// Gets the current phase. 35 | /// 36 | /// 37 | /// The current phase. 38 | /// 39 | public long CurrentPhase { get; private set; } 40 | 41 | /// 42 | /// Advances this phase instance. 43 | /// 44 | /// The comment for Test visual identification Purposes only. 45 | /// 46 | /// A representing the asynchronous operation. 47 | /// 48 | public Task AdvancePhaseAsync(string comment = "") 49 | { 50 | if (_phaseSync.ParticipantCount == _phaseSync.ParticipantsRemaining) 51 | { 52 | CurrentPhase = CompletedPhases + 1; 53 | } 54 | 55 | return Task.Run(() => _phaseSync?.SignalAndWait(CancellationToken.None)); 56 | } 57 | 58 | /// 59 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 60 | /// 61 | public void Dispose() 62 | { 63 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 64 | Dispose(disposing: true); 65 | GC.SuppressFinalize(this); 66 | } 67 | 68 | /// 69 | /// Releases unmanaged and - optionally - managed resources. 70 | /// 71 | /// true to release both managed and unmanaged resources; false to release only unmanaged resources. 72 | protected virtual void Dispose(bool disposing) 73 | { 74 | if (!_disposedValue) 75 | { 76 | if (disposing) 77 | { 78 | _phaseSync.Dispose(); 79 | } 80 | 81 | _disposedValue = true; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32630.192 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.Mvvm", "ReactiveMarbles.Mvvm\ReactiveMarbles.Mvvm.csproj", "{12FCC761-EA3D-4443-9D88-BE03B3D6C802}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.Mvvm.Tests", "ReactiveMarbles.Mvvm.Tests\ReactiveMarbles.Mvvm.Tests.csproj", "{1179097F-2B50-4892-8FA7-CD2AFC5E6D3D}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.Mvvm.Benchmarks", "ReactiveMarbles.Mvvm.Benchmarks\ReactiveMarbles.Mvvm.Benchmarks.csproj", "{16D18273-3F1A-4596-9324-0B6CC951E1BB}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5957A7E7-ADF4-43BA-B555-3F7DD05A72A2}" 13 | ProjectSection(SolutionItems) = preProject 14 | ..\.editorconfig = ..\.editorconfig 15 | directory.build.props = directory.build.props 16 | directory.build.targets = directory.build.targets 17 | directory.packages.props = directory.packages.props 18 | nuget.config = nuget.config 19 | stylecop.json = stylecop.json 20 | EndProjectSection 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {12FCC761-EA3D-4443-9D88-BE03B3D6C802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {12FCC761-EA3D-4443-9D88-BE03B3D6C802}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {12FCC761-EA3D-4443-9D88-BE03B3D6C802}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {12FCC761-EA3D-4443-9D88-BE03B3D6C802}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {1179097F-2B50-4892-8FA7-CD2AFC5E6D3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {1179097F-2B50-4892-8FA7-CD2AFC5E6D3D}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {1179097F-2B50-4892-8FA7-CD2AFC5E6D3D}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {1179097F-2B50-4892-8FA7-CD2AFC5E6D3D}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {16D18273-3F1A-4596-9324-0B6CC951E1BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {16D18273-3F1A-4596-9324-0B6CC951E1BB}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {16D18273-3F1A-4596-9324-0B6CC951E1BB}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {16D18273-3F1A-4596-9324-0B6CC951E1BB}.Release|Any CPU.Build.0 = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(SolutionProperties) = preSolution 42 | HideSolutionNode = FALSE 43 | EndGlobalSection 44 | GlobalSection(ExtensibilityGlobals) = postSolution 45 | SolutionGuid = {DE26C39D-3A31-4AD0-B642-EDF83893FE48} 46 | EndGlobalSection 47 | EndGlobal 48 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/AsValueExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Concurrency; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | /// 11 | /// Extensions methods that provider a binder implementation for observable values. 12 | /// 13 | public static class AsValueExtensions 14 | { 15 | /// 16 | /// Projects an observable value to a property for binding. 17 | /// 18 | /// The source observable. 19 | /// Callback with the changed value. 20 | /// The property type. 21 | /// A binder. 22 | public static IValueBinder AsValue(this IObservable source, Action onChanged) => 23 | new ValueBinder(source, onChanged, initialValue: () => default); 24 | 25 | /// 26 | /// Projects an observable value to a property for binding. 27 | /// 28 | /// The source observable. 29 | /// Callback with the changed value. 30 | /// The function that provides the initial value. 31 | /// The property type. 32 | /// A binder. 33 | public static IValueBinder AsValue(this IObservable source, Action onChanged, Func initialValue) => 34 | new ValueBinder(source, onChanged, initialValue); 35 | 36 | /// 37 | /// Projects an observable value to a property for binding. 38 | /// 39 | /// The source observable. 40 | /// Callback with the changed value. 41 | /// The scheduler instance where the value will output. 42 | /// The function that provides the initial value. 43 | /// The property type. 44 | /// A binder. 45 | public static IValueBinder AsValue(this IObservable source, Action onChanged, IScheduler scheduler, Func initialValue) => 46 | new ValueBinder(source, onChanged, scheduler, initialValue); 47 | 48 | /// 49 | /// Projects an observable value to a property for binding. 50 | /// 51 | /// The source observable. 52 | /// Callback with the changing value. 53 | /// Callback with the changed value. 54 | /// The function that provides the initial value. 55 | /// The property type. 56 | /// A binder. 57 | public static IValueBinder AsValue(this IObservable source, Action onChanging, Action onChanged, Func initialValue) => 58 | new ValueBinder(source, onChanging, onChanged, initialValue); 59 | 60 | /// 61 | /// Projects an observable value to a property for binding. 62 | /// 63 | /// The source observable. 64 | /// Callback with the changing value. 65 | /// Callback with the changed value. 66 | /// The scheduler instance where the value will output. 67 | /// The function that provides the initial value. 68 | /// The property type. 69 | /// A binder. 70 | public static IValueBinder AsValue(this IObservable source, Action onChanging, Action onChanged, IScheduler scheduler, Func initialValue) => 71 | new ValueBinder(source, onChanging, onChanged, scheduler, initialValue); 72 | 73 | /// 74 | /// Projects an observable value to a property for binding. 75 | /// This value is lazy and will not subscribe until first access. 76 | /// 77 | /// The source observable. 78 | /// Callback with the changed value. 79 | /// The property type. 80 | /// A binder. 81 | public static IValueBinder AsLazyValue(this IObservable source, Action onChanged) => 82 | new LazyValueBinder(source, onChanged, initialValue: () => default); 83 | 84 | /// 85 | /// Projects an observable value to a property for binding. 86 | /// This value is lazy and will not subscribe until first access. 87 | /// 88 | /// The source observable. 89 | /// Callback with the changed value. 90 | /// The function that provides the initial value. 91 | /// The property type. 92 | /// A binder. 93 | public static IValueBinder AsLazyValue(this IObservable source, Action onChanged, Func initialValue) => 94 | new LazyValueBinder(source, onChanged, initialValue); 95 | 96 | /// 97 | /// Projects an observable value to a property for binding. 98 | /// This value is lazy and will not subscribe until first access. 99 | /// 100 | /// The source observable. 101 | /// Callback with the changed value. 102 | /// The scheduler instance where the value will output. 103 | /// The function that provides the initial value. 104 | /// The property type. 105 | /// A binder. 106 | public static IValueBinder AsLazyValue(this IObservable source, Action onChanged, IScheduler scheduler, Func initialValue) => 107 | new LazyValueBinder(source, onChanged, scheduler, initialValue); 108 | 109 | /// 110 | /// Projects an observable value to a property for binding. 111 | /// This value is lazy and will not subscribe until first access. 112 | /// 113 | /// The source observable. 114 | /// Callback with the changing value. 115 | /// Callback with the changed value. 116 | /// The function that provides the initial value. 117 | /// The property type. 118 | /// A binder. 119 | public static IValueBinder AsLazyValue(this IObservable source, Action onChanging, Action onChanged, Func initialValue) => 120 | new LazyValueBinder(source, onChanging, onChanged, initialValue); 121 | 122 | /// 123 | /// Projects an observable value to a property for binding. 124 | /// This value is lazy and will not subscribe until first access. 125 | /// 126 | /// The source observable. 127 | /// Callback with the changing value. 128 | /// Callback with the changed value. 129 | /// The scheduler instance where the value will output. 130 | /// The function that provides the initial value. 131 | /// The property type. 132 | /// A binder. 133 | public static IValueBinder AsLazyValue(this IObservable source, Action onChanging, Action onChanged, IScheduler scheduler, Func initialValue) => 134 | new LazyValueBinder(source, onChanging, onChanged, scheduler, initialValue); 135 | } 136 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/CoreRegistration.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Concurrency; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | internal class CoreRegistration(IScheduler mainThreadScheduler, IScheduler taskPoolScheduler, IObserver exceptionHandler) : ICoreRegistration 11 | { 12 | /// 13 | public IScheduler MainThreadScheduler { get; } = mainThreadScheduler; 14 | 15 | /// 16 | public IScheduler TaskPoolScheduler { get; } = taskPoolScheduler; 17 | 18 | /// 19 | public IObserver ExceptionHandler { get; } = exceptionHandler; 20 | } 21 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/CoreRegistrationBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Concurrency; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | /// 11 | /// Builder for . 12 | /// 13 | public class CoreRegistrationBuilder 14 | { 15 | private IScheduler _mainThread = DefaultScheduler.Instance; 16 | private IScheduler _taskPool = TaskPoolScheduler.Default; 17 | private IObserver _exceptionHandler = new DebugExceptionHandler(); 18 | 19 | /// 20 | /// Returns a static builder extension to create an . 21 | /// 22 | /// The builder. 23 | public static CoreRegistrationBuilder Create() => new(); 24 | 25 | /// 26 | /// Adds a main thread scheduler to the . 27 | /// 28 | /// The main thread scheduler. 29 | /// The builder. 30 | public CoreRegistrationBuilder WithMainThreadScheduler(IScheduler scheduler) 31 | { 32 | _mainThread = scheduler; 33 | return this; 34 | } 35 | 36 | /// 37 | /// Adds a to the core registration. 38 | /// 39 | /// The task pool scheduler. 40 | /// The builder. 41 | public CoreRegistrationBuilder WithTaskPoolScheduler(IScheduler scheduler) 42 | { 43 | _taskPool = scheduler; 44 | return this; 45 | } 46 | 47 | /// 48 | /// Adds an exception handler to the . 49 | /// 50 | /// The exception handler. 51 | /// The builder. 52 | public CoreRegistrationBuilder WithExceptionHandler(IObserver handler) 53 | { 54 | _exceptionHandler = handler; 55 | return this; 56 | } 57 | 58 | /// 59 | /// Builds an . 60 | /// 61 | /// The core registration. 62 | public ICoreRegistration Build() => new CoreRegistration(_mainThread, _taskPool, _exceptionHandler); 63 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/DebugExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Diagnostics; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | /// 11 | /// Debug exception handler. 12 | /// This is the default exception handler. 13 | /// 14 | public class DebugExceptionHandler : IObserver 15 | { 16 | /// 17 | public void OnCompleted() 18 | { 19 | if (Debugger.IsAttached) 20 | { 21 | Debugger.Break(); 22 | } 23 | } 24 | 25 | /// 26 | public void OnError(Exception error) 27 | { 28 | if (Debugger.IsAttached) 29 | { 30 | Debugger.Break(); 31 | } 32 | } 33 | 34 | /// 35 | public void OnNext(Exception value) 36 | { 37 | if (Debugger.IsAttached) 38 | { 39 | Debugger.Break(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ICoreRegistration.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Concurrency; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | /// 11 | /// Interface that represents core Reactive Marbles registration. 12 | /// 13 | public interface ICoreRegistration 14 | { 15 | /// 16 | /// Gets the main thread scheduler. 17 | /// 18 | IScheduler MainThreadScheduler { get; } 19 | 20 | /// 21 | /// Gets the task pool scheduler. 22 | /// 23 | IScheduler TaskPoolScheduler { get; } 24 | 25 | /// 26 | /// Gets the default exception handler. 27 | /// 28 | IObserver ExceptionHandler { get; } 29 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ILoggable.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.Mvvm; 6 | 7 | /// 8 | /// Marker interface for classes that want access to the underlying framework logger. 9 | /// 10 | public interface ILoggable; 11 | 12 | // TODO: [rlittlesii: October 11, 2021] US MS Logging to log MVVM internals. 13 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IRxObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.ComponentModel; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | /// 11 | /// A reactive object is a interface for ViewModels which will expose 12 | /// logging, and notify when properties are either changing or changed. 13 | /// The primary use of this interface is to allow external classes such as 14 | /// the ObservableAsPropertyHelper to trigger these events inside the ViewModel. 15 | /// 16 | public interface IRxObject : INotifyPropertyChanged, INotifyPropertyChanging, IThrownExceptions 17 | { 18 | /// 19 | /// Gets an observable that fires *before* a property is about to 20 | /// be changed. Note that this should not fire duplicate change notifications if a 21 | /// property is set to the same value multiple times. 22 | /// 23 | IObservable> Changing { get; } 24 | 25 | /// 26 | /// Gets an Observable that fires *after* a property has changed. 27 | /// Note that this should not fire duplicate change notifications if a 28 | /// property is set to the same value multiple times. 29 | /// 30 | IObservable> Changed { get; } 31 | 32 | /// 33 | /// When this method is called, an object will not fire change 34 | /// notifications (neither traditional nor Observable notifications) 35 | /// until the return value is disposed. 36 | /// 37 | /// An object that, when disposed, re-enables change 38 | /// notifications. 39 | IDisposable SuppressChangeNotifications(); 40 | 41 | /// 42 | /// Determines if change notifications are enabled or not. 43 | /// 44 | /// A value indicating whether change notifications are enabled. 45 | bool AreChangeNotificationsEnabled(); 46 | 47 | /// 48 | /// Gets a value indicating whether the change notifications are delayed. 49 | /// 50 | /// A truth value. 51 | bool AreChangeNotificationsDelayed(); 52 | 53 | /// 54 | /// Delays notifications until the return IDisposable is disposed. 55 | /// 56 | /// A disposable which when disposed will send delayed notifications. 57 | IDisposable DelayChangeNotifications(); 58 | } 59 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IRxPropertyEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.Mvvm; 6 | 7 | /// 8 | /// Interface representing property changing and changed event arguments. 9 | /// 10 | /// The sender type. 11 | public interface IRxPropertyEventArgs 12 | { 13 | /// 14 | /// Gets the property name that changed. 15 | /// 16 | string? PropertyName { get; } 17 | 18 | /// 19 | /// Gets the sender that changed. 20 | /// 21 | TSender Sender { get; } 22 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IRxPropertyEventArgsExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | internal static class IRxPropertyEventArgsExtensions 11 | { 12 | /// 13 | /// Filter a list of change notifications, returning the last change for each PropertyName in original order. 14 | /// 15 | public static IEnumerable DistinctEvents(this IEnumerable events) 16 | where TEventArgs : IRxPropertyEventArgs 17 | { 18 | var eventArgsList = events.ToList(); 19 | if (eventArgsList.Count <= 1) 20 | { 21 | return eventArgsList; 22 | } 23 | 24 | var seen = new HashSet(); 25 | var uniqueEvents = new Stack(eventArgsList.Count); 26 | 27 | for (var i = eventArgsList.Count - 1; i >= 0; i--) 28 | { 29 | var propertyName = eventArgsList[i].PropertyName; 30 | if (propertyName is not null && seen.Add(propertyName)) 31 | { 32 | uniqueEvents.Push(eventArgsList[i]); 33 | } 34 | } 35 | 36 | // Stack enumerates in LIFO order 37 | return uniqueEvents; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IStateHandler.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | 10 | /// 11 | /// ISuspensionDriver represents a class that can load/save state to persistent 12 | /// storage. Most platforms have a basic implementation of this class, but you 13 | /// probably want to write your own. 14 | /// 15 | public interface IStateHandler 16 | { 17 | /// 18 | /// Loads the application state from persistent storage. 19 | /// 20 | /// An object observable. 21 | IObservable LoadState(); 22 | 23 | /// 24 | /// Saves the application state to disk. 25 | /// 26 | /// The application state. 27 | /// A completed observable. 28 | IObservable SaveState(object state); 29 | 30 | /// 31 | /// Invalidates the application state (i.e. deletes it from disk). 32 | /// 33 | /// A completed observable. 34 | IObservable InvalidateState(); 35 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IStateHost.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive; 7 | 8 | namespace ReactiveMarbles.Mvvm; 9 | /* Nicked from http://caliburnmicro.codeplex.com/wikipage?title=Working%20with%20Windows%20Phone%207%20v1.1 10 | * 11 | * Launching - Occurs when a fresh instance of the application is launching. 12 | * Activated - Occurs when a previously paused/tombstoned app is resumed/resurrected. 13 | * Deactivated - Occurs when the application is being paused or tombstoned. 14 | * Closing - Occurs when the application is closing. 15 | * Continuing - Occurs when the app is continuing from a temporarily paused state. 16 | * Continued - Occurs after the app has continued from a temporarily paused state. 17 | * Resurrecting - Occurs when the app is "resurrecting" from a tombstoned state. 18 | * Resurrected - Occurs after the app has "resurrected" from a tombstoned state. 19 | */ 20 | 21 | /// 22 | /// ISuspensionHost represents a standardized version of the events that the 23 | /// host operating system publishes. Subscribe to these events in order to 24 | /// handle app suspend / resume. 25 | /// 26 | public interface IStateHost : IRxObject 27 | { 28 | /// 29 | /// Gets or sets the observable which signals when the application is launching new. This can happen when 30 | /// an app has recently crashed, as well as the first time the app has 31 | /// been launched. Apps should create their state from scratch. 32 | /// 33 | IObservable IsLaunchingNew { get; set; } 34 | 35 | /// 36 | /// Gets or sets the observable which signals when the application is resuming from suspended state (i.e. 37 | /// it was previously running but its process was destroyed). 38 | /// 39 | IObservable IsResuming { get; set; } 40 | 41 | /// 42 | /// Gets or sets the observable which signals when the application is activated. Note that this may mean 43 | /// that your process was not actively running before this signal. 44 | /// 45 | IObservable IsUnpausing { get; set; } 46 | 47 | /// 48 | /// Gets or sets the observable which signals when the application should persist its state to disk. 49 | /// 50 | /// Returns an IDisposable that should be disposed once the 51 | /// application finishes persisting its state. 52 | IObservable ShouldPersistState { get; set; } 53 | 54 | /// 55 | /// Gets or sets the observable which signals that the saved application state should be deleted, this 56 | /// usually is called after an app has crashed. 57 | /// 58 | IObservable ShouldInvalidateState { get; set; } 59 | 60 | /// 61 | /// Gets or sets a function that can be used to create a new application state - usually 62 | /// this method just calls 'new' on an object. 63 | /// 64 | Func? CreateNewAppState { get; set; } 65 | 66 | /// 67 | /// Gets or sets the current application state. 68 | /// The "application state" is a notion entirely defined 69 | /// via the client application - the framework places no restrictions on 70 | /// the object other than it can be serialized. 71 | /// 72 | object? AppState { get; set; } 73 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IThrownExceptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace ReactiveMarbles.Mvvm; 8 | 9 | /// 10 | /// 11 | /// This interface is implemented by Rx objects which are given 12 | /// IObservables as input - when the input IObservables OnError, instead of 13 | /// disabling the Rx object, we catch the IObservable and pipe it into 14 | /// this property. 15 | /// 16 | /// 17 | /// Normally this IObservable is implemented with a ScheduledSubject whose 18 | /// default Observer is CoreRegistration.DefaultExceptionHandler - this means, that if 19 | /// you aren't listening to ThrownExceptions and one appears, the exception 20 | /// will appear on the UI thread and crash the application. 21 | /// 22 | /// 23 | public interface IThrownExceptions 24 | { 25 | /// 26 | /// Gets a observable which will fire whenever an exception would normally terminate. 27 | /// internal state. 28 | /// 29 | IObservable ThrownExceptions { get; } 30 | } 31 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IValueBinder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace ReactiveMarbles.Mvvm; 8 | 9 | /// 10 | /// Represents a typed value for binding to UI elements. 11 | /// 12 | /// The value type. 13 | public interface IValueBinder : IDisposable 14 | { 15 | /// 16 | /// Gets the latest value. 17 | /// 18 | T Value { get; } 19 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/IsExternalInit.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | 7 | // ReSharper disable once CheckNamespace 8 | namespace System.Runtime.CompilerServices; 9 | 10 | /// 11 | /// Reserved to be used by the compiler for tracking metadata. 12 | /// This class should not be used by developers in source code. 13 | /// 14 | [EditorBrowsable(EditorBrowsableState.Never)] 15 | internal static class IsExternalInit; 16 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/LazyValueBinder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Concurrency; 7 | using System.Reactive.Disposables; 8 | using System.Reactive.Linq; 9 | using System.Threading; 10 | using ReactiveMarbles.Extensions; 11 | 12 | namespace ReactiveMarbles.Mvvm; 13 | 14 | /// 15 | /// is a class to help produce a bindable value from an observable sequence. This class 16 | /// will not produce a value until an observer subscribes to the value. This property is read-only and will fire change 17 | /// notifications. Using the methods allows easy creation from an observable sequence. 18 | /// 19 | /// The value type. 20 | internal sealed class LazyValueBinder : IValueBinder 21 | { 22 | private readonly CompositeDisposable _disposable = []; 23 | private readonly Func _initialValue; 24 | private Action? _lazy; 25 | private T? _value; 26 | 27 | /// 28 | /// Initializes a new instance of the class. 29 | /// 30 | /// The source observable. 31 | /// The on changing delegate. 32 | /// The on changed delegate. 33 | /// The thread scheduler. 34 | /// The function that provides an initial value. 35 | public LazyValueBinder( 36 | IObservable source, 37 | Action? onChanging, 38 | Action onChanged, 39 | IScheduler? scheduler, 40 | Func initialValue) 41 | { 42 | _initialValue = initialValue; 43 | var subject = new ProxyScheduledSubject(scheduler ?? CurrentThreadScheduler.Instance); 44 | _lazy = () => 45 | { 46 | _value = _initialValue.Invoke(); 47 | _ = subject 48 | .Subscribe(value => 49 | { 50 | onChanging?.Invoke(value); 51 | _value = value; 52 | onChanged.Invoke(value); 53 | }).DisposeWith(_disposable); 54 | _ = source.DistinctUntilChanged().StartWith(_value).Subscribe(subject).DisposeWith(_disposable); 55 | }; 56 | } 57 | 58 | /// 59 | /// Initializes a new instance of the class. 60 | /// 61 | /// The source observable. 62 | /// The on changed delegate. 63 | /// The function that provides an initial value. 64 | public LazyValueBinder(IObservable source, Action onChanged, Func initialValue) 65 | : this(source, null, onChanged, null, initialValue) 66 | { 67 | } 68 | 69 | /// 70 | /// Initializes a new instance of the class. 71 | /// 72 | /// The source observable. 73 | /// The on changing delegate. 74 | /// The on changed delegate. 75 | /// The function that provides an initial value. 76 | public LazyValueBinder(IObservable source, Action onChanging, Action onChanged, Func initialValue) 77 | : this(source, onChanging, onChanged, null, initialValue) 78 | { 79 | } 80 | 81 | /// 82 | /// Initializes a new instance of the class. 83 | /// 84 | /// The source observable. 85 | /// The on changed delegate. 86 | /// The thread scheduler. 87 | /// The function that provides an initial value. 88 | public LazyValueBinder(IObservable source, Action onChanged, IScheduler scheduler, Func initialValue) 89 | : this(source, null, onChanged, scheduler, initialValue) 90 | { 91 | } 92 | 93 | /// 94 | /// Gets the latest value. 95 | /// 96 | public T Value 97 | { 98 | get 99 | { 100 | Interlocked.Exchange(ref _lazy, null)?.Invoke(); 101 | return _value ?? _initialValue.Invoke(); 102 | } 103 | } 104 | 105 | /// 106 | public void Dispose() => _disposable.Dispose(); 107 | } 108 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/LocatorExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Reactive.Concurrency; 8 | 9 | using ReactiveMarbles.Locator; 10 | 11 | namespace ReactiveMarbles.Mvvm; 12 | 13 | /// 14 | /// extensions. 15 | /// 16 | [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1625:ElementDocumentationMustNotBeCopiedAndPasted", Justification = "Builder Extensions.")] 17 | public static class LocatorExtensions 18 | { 19 | /// 20 | /// Adds core registrations to the locator. 21 | /// 22 | /// The service locator. 23 | /// The main thread scheduler. 24 | /// The task pool scheduler. 25 | /// The exception handler. 26 | /// The service locator. 27 | public static IServiceLocator AddCoreRegistrations( 28 | this IServiceLocator serviceLocator, 29 | IScheduler mainThreadScheduler, 30 | IScheduler taskPoolScheduler, 31 | IObserver exceptionHandler) => 32 | serviceLocator 33 | .AddCoreRegistrations(() => 34 | new CoreRegistration( 35 | mainThreadScheduler, 36 | taskPoolScheduler, 37 | exceptionHandler)); 38 | 39 | /// 40 | /// Adds core registrations to the locator. 41 | /// 42 | /// The service locator. 43 | /// The core registration. 44 | /// The service locator. 45 | public static IServiceLocator AddCoreRegistrations(this IServiceLocator serviceLocator, Func coreRegistration) 46 | { 47 | serviceLocator.AddService(coreRegistration); 48 | return serviceLocator; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/Notifications.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.Diagnostics.CodeAnalysis; 6 | 7 | using DynamicData; 8 | 9 | namespace ReactiveMarbles.Mvvm; 10 | 11 | [SuppressMessage("StyleCop", "SA1401", Justification = "Deliberate use of private field")] 12 | internal class Notifications 13 | { 14 | public readonly SourceList> PropertyChangedEvents = new(); 15 | public readonly SourceList> PropertyChangingEvents = new(); 16 | public long ChangeNotificationsDelayed; 17 | public long ChangeNotificationsSuppressed; 18 | } 19 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ProxyScheduledSubject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Concurrency; 7 | using System.Reactive.Disposables; 8 | using System.Reactive.Linq; 9 | using System.Reactive.Subjects; 10 | using System.Threading; 11 | using ReactiveMarbles.Extensions; 12 | 13 | namespace ReactiveMarbles.Mvvm; 14 | 15 | /// 16 | /// A subject which dispatches all its events on the specified Scheduler. 17 | /// 18 | /// The type of item being dispatched by the Subject. 19 | public class ProxyScheduledSubject : ISubject, IDisposable 20 | { 21 | private readonly CompositeDisposable _disposables = []; 22 | private readonly IObserver _defaultObserver; 23 | private readonly IScheduler _scheduler; 24 | private readonly ISubject _subject; 25 | private int _observerRefCount; 26 | private IDisposable _defaultObserverSub = Disposable.Empty; 27 | 28 | /// 29 | /// Initializes a new instance of the class. 30 | /// 31 | /// The scheduler where to dispatch items to. 32 | /// A optional default observer where notifications will be sent. 33 | /// A optional default subject which this Subject will wrap. 34 | public ProxyScheduledSubject(IScheduler scheduler, IObserver? defaultObserver = null, ISubject? defaultSubject = null) 35 | { 36 | _scheduler = scheduler; 37 | _defaultObserver = defaultObserver ?? new Subject(); 38 | _subject = defaultSubject ?? new Subject(); 39 | 40 | if (defaultObserver is not null) 41 | { 42 | _defaultObserverSub = _subject.ObserveOn(_scheduler).Subscribe(_defaultObserver).DisposeWith(_disposables); 43 | } 44 | } 45 | 46 | /// 47 | public void Dispose() 48 | { 49 | Dispose(true); 50 | GC.SuppressFinalize(this); 51 | } 52 | 53 | /// 54 | public void OnCompleted() => _subject.OnCompleted(); 55 | 56 | /// 57 | public void OnError(Exception error) => _subject.OnError(error); 58 | 59 | /// 60 | public void OnNext(T value) => _subject.OnNext(value); 61 | 62 | /// 63 | public IDisposable Subscribe(IObserver observer) 64 | { 65 | Interlocked.Exchange(ref _defaultObserverSub, Disposable.Empty).Dispose(); 66 | 67 | _ = Interlocked.Increment(ref _observerRefCount); 68 | 69 | return new CompositeDisposable( 70 | _subject.ObserveOn(_scheduler).Subscribe(observer), 71 | Disposable.Create(() => 72 | { 73 | if (Interlocked.Decrement(ref _observerRefCount) <= 0) 74 | { 75 | _defaultObserverSub = _subject.ObserveOn(_scheduler).Subscribe(_defaultObserver); 76 | } 77 | })); 78 | } 79 | 80 | /// 81 | /// Disposes of any managed resources in our class. 82 | /// 83 | /// If we are being called by the IDisposable method. 84 | protected virtual void Dispose(bool isDisposing) 85 | { 86 | if (isDisposing) 87 | { 88 | if (_subject is IDisposable disposable) 89 | { 90 | disposable.Dispose(); 91 | } 92 | 93 | _disposables.Dispose(); 94 | _defaultObserverSub.Dispose(); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ReactiveMarbles.Mvvm.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net6.0;net8.0 5 | latest 6 | A MVVM framework that integrates with the Reactive Extensions for .NET to create elegant, testable MVVM components that run on any mobile or desktop platform. This is the base package with the base platform implementations 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ReactiveProperty/IReactiveProperty.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections; 7 | using System.ComponentModel; 8 | using System.Reactive.Disposables; 9 | 10 | namespace ReactiveMarbles.Mvvm; 11 | 12 | /// 13 | /// Reactive Property. 14 | /// 15 | /// The type of the property. 16 | /// 17 | /// 18 | public interface IReactiveProperty : IObservable, ICancelable, INotifyDataErrorInfo, INotifyPropertyChanged 19 | { 20 | /// 21 | /// Gets or sets the value. 22 | /// 23 | /// 24 | /// The value. 25 | /// 26 | public T? Value { get; set; } 27 | 28 | /// 29 | /// Gets the observe error changed. 30 | /// 31 | /// The observe error changed. 32 | IObservable ObserveErrorChanged { get; } 33 | 34 | /// 35 | /// Gets the observe has errors. 36 | /// 37 | /// The observe has errors. 38 | IObservable ObserveHasErrors { get; } 39 | 40 | /// 41 | /// Refreshes this instance. 42 | /// 43 | void Refresh(); 44 | } 45 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ReactiveProperty/ReactivePropertyMixins.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Linq.Expressions; 9 | using System.Reactive.Linq; 10 | using System.Reflection; 11 | 12 | namespace ReactiveMarbles.Mvvm; 13 | 14 | /// 15 | /// Reactive Property Extensions. 16 | /// 17 | public static class ReactivePropertyMixins 18 | { 19 | /// 20 | /// Set validation logic from DataAnnotations attributes. 21 | /// 22 | /// Property type. 23 | /// Target ReactiveProperty. 24 | /// The self selector. 25 | /// 26 | /// Self. 27 | /// 28 | /// 29 | /// selfSelector 30 | /// or 31 | /// self. 32 | /// 33 | public static ReactiveProperty AddValidation(this ReactiveProperty self, Expression?>> selfSelector) 34 | { 35 | if (selfSelector == null) 36 | { 37 | throw new ArgumentNullException(nameof(selfSelector)); 38 | } 39 | 40 | if (self == null) 41 | { 42 | throw new ArgumentNullException(nameof(self)); 43 | } 44 | 45 | var memberExpression = (MemberExpression)selfSelector.Body; 46 | var propertyInfo = (PropertyInfo)memberExpression.Member; 47 | var display = propertyInfo.GetCustomAttribute(); 48 | var attrs = propertyInfo.GetCustomAttributes().ToArray(); 49 | var context = new System.ComponentModel.DataAnnotations.ValidationContext(self) 50 | { 51 | DisplayName = display?.GetName() ?? propertyInfo.Name, 52 | MemberName = nameof(ReactiveProperty.Value), 53 | }; 54 | 55 | if (attrs.Length != 0) 56 | { 57 | self.AddValidationError(x => 58 | { 59 | var validationResults = new List(); 60 | if (System.ComponentModel.DataAnnotations.Validator.TryValidateValue(x!, context, validationResults, attrs)) 61 | { 62 | return null; 63 | } 64 | 65 | return validationResults[0].ErrorMessage; 66 | }); 67 | } 68 | 69 | return self; 70 | } 71 | 72 | /// 73 | /// Create an IObservable instance to observe validation error messages of ReactiveProperty. 74 | /// 75 | /// Property type. 76 | /// Target ReactiveProperty. 77 | /// A IObservable of string. 78 | public static IObservable ObserveValidationErrors(this ReactiveProperty self) 79 | { 80 | if (self == null) 81 | { 82 | throw new ArgumentNullException(nameof(self)); 83 | } 84 | 85 | return self.ObserveErrorChanged 86 | .Select(x => x?.OfType()?.FirstOrDefault()); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ReactiveProperty/SingletonDataErrorsChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | 7 | namespace ReactiveMarbles.Mvvm; 8 | 9 | internal static class SingletonDataErrorsChangedEventArgs 10 | { 11 | public static readonly DataErrorsChangedEventArgs Value = new(nameof(Value)); 12 | } 13 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ReactiveProperty/SingletonPropertyChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System.ComponentModel; 6 | 7 | namespace ReactiveMarbles.Mvvm; 8 | 9 | internal static class SingletonPropertyChangedEventArgs 10 | { 11 | public static readonly PropertyChangedEventArgs Value = new(nameof(Value)); 12 | public static readonly PropertyChangedEventArgs HasErrors = new(nameof(INotifyDataErrorInfo.HasErrors)); 13 | public static readonly PropertyChangedEventArgs ErrorMessage = new(nameof(ErrorMessage)); 14 | } 15 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/RxDisposableObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace ReactiveMarbles.Mvvm; 8 | 9 | /// 10 | /// Rx object that extends the interface. 11 | /// 12 | public class RxDisposableObject : RxObject, IDisposable 13 | { 14 | /// 15 | public void Dispose() 16 | { 17 | Dispose(true); 18 | GC.SuppressFinalize(this); 19 | } 20 | 21 | /// 22 | /// Disposes of the resources. 23 | /// 24 | /// Disposing. 25 | protected virtual void Dispose(bool disposing) 26 | { 27 | } 28 | } -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/RxObject.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.ComponentModel; 8 | using System.Reactive.Concurrency; 9 | using System.Reactive.Disposables; 10 | using System.Reactive.Linq; 11 | using System.Reactive.Subjects; 12 | using System.Runtime.CompilerServices; 13 | using System.Threading; 14 | 15 | using DynamicData; 16 | 17 | using ReactiveMarbles.Locator; 18 | 19 | namespace ReactiveMarbles.Mvvm; 20 | 21 | /// 22 | /// is the base object for ViewModel classes, and it 23 | /// implements . In addition, provides 24 | /// Changing and Changed Observables to monitor object changes. 25 | /// 26 | public class RxObject : IRxObject 27 | { 28 | private const string InvalidOperationMessage = "Cannot cast Sender to an IRxObject"; 29 | private readonly Lazy> _thrownExceptions = new( 30 | () => new ProxyScheduledSubject(Scheduler.Immediate, ServiceLocator.Current().GetService().ExceptionHandler), LazyThreadSafetyMode.PublicationOnly); 31 | 32 | private readonly Lazy _notification = new(() => new Notifications()); 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | protected RxObject() 38 | { 39 | Changed = Observable.Create>(observer => 40 | { 41 | void Handler(object? sender, PropertyChangedEventArgs args) => 42 | observer.OnNext(new RxPropertyChangedEventArgs(args.PropertyName, sender as IRxObject ?? throw new InvalidOperationException(InvalidOperationMessage))); 43 | 44 | PropertyChanged += Handler; 45 | return Disposable.Create(() => PropertyChanged -= Handler); 46 | }); 47 | 48 | Changing = Observable.Create>(observer => 49 | { 50 | void Handler(object? sender, PropertyChangingEventArgs args) => 51 | observer.OnNext(new RxPropertyChangingEventArgs(args.PropertyName, sender as IRxObject ?? throw new InvalidOperationException(InvalidOperationMessage))); 52 | 53 | PropertyChanging += Handler; 54 | return Disposable.Create(() => PropertyChanging -= Handler); 55 | }); 56 | } 57 | 58 | /// 59 | public event PropertyChangedEventHandler? PropertyChanged; 60 | 61 | /// 62 | public event PropertyChangingEventHandler? PropertyChanging; 63 | 64 | /// 65 | public IObservable ThrownExceptions => _thrownExceptions.Value.AsObservable(); 66 | 67 | /// 68 | public IObservable> Changing { get; } 69 | 70 | /// 71 | public IObservable> Changed { get; } 72 | 73 | /// 74 | public bool AreChangeNotificationsEnabled() => !_notification.IsValueCreated || Interlocked.Read(ref _notification.Value.ChangeNotificationsSuppressed) == 0; 75 | 76 | /// 77 | public bool AreChangeNotificationsDelayed() => _notification.IsValueCreated && Interlocked.Read(ref _notification.Value.ChangeNotificationsDelayed) > 0; 78 | 79 | /// 80 | public IDisposable SuppressChangeNotifications() 81 | { 82 | _ = Interlocked.Increment(ref _notification.Value.ChangeNotificationsSuppressed); 83 | return Disposable.Create(() => Interlocked.Decrement(ref _notification.Value.ChangeNotificationsSuppressed)); 84 | } 85 | 86 | /// 87 | public IDisposable DelayChangeNotifications() 88 | { 89 | _ = Interlocked.Increment(ref _notification.Value.ChangeNotificationsDelayed); 90 | 91 | return Disposable.Create(() => 92 | { 93 | if (Interlocked.Decrement(ref _notification.Value.ChangeNotificationsDelayed) == 0) 94 | { 95 | foreach (var distinctEvent in _notification.Value.PropertyChangingEvents.Items.DistinctEvents()) 96 | { 97 | PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(distinctEvent.PropertyName)); 98 | } 99 | 100 | foreach (var distinctEvent in _notification.Value.PropertyChangedEvents.Items.DistinctEvents()) 101 | { 102 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(distinctEvent.PropertyName)); 103 | } 104 | 105 | _notification.Value.PropertyChangingEvents.Clear(); 106 | _notification.Value.PropertyChangedEvents.Clear(); 107 | } 108 | }); 109 | } 110 | 111 | /// 112 | /// Raises the property changing event. 113 | /// 114 | /// The argument. 115 | protected void RaisePropertyChanging(PropertyChangingEventArgs args) 116 | { 117 | if (!AreChangeNotificationsEnabled()) 118 | { 119 | return; 120 | } 121 | 122 | if (AreChangeNotificationsDelayed()) 123 | { 124 | _notification.Value.PropertyChangingEvents.Add(new RxPropertyChangingEventArgs(args.PropertyName, this)); 125 | return; 126 | } 127 | 128 | try 129 | { 130 | PropertyChanging?.Invoke(this, args); 131 | } 132 | catch (Exception e) when (_thrownExceptions.IsValueCreated) 133 | { 134 | _thrownExceptions.Value.OnNext(e); 135 | } 136 | } 137 | 138 | /// 139 | /// Raises the property changed event. 140 | /// 141 | /// The argument. 142 | protected void RaisePropertyChanged(PropertyChangedEventArgs args) 143 | { 144 | if (!AreChangeNotificationsEnabled()) 145 | { 146 | return; 147 | } 148 | 149 | if (AreChangeNotificationsDelayed()) 150 | { 151 | _notification.Value.PropertyChangedEvents.Add(new RxPropertyChangedEventArgs(args.PropertyName, this)); 152 | return; 153 | } 154 | 155 | try 156 | { 157 | PropertyChanged?.Invoke(this, args); 158 | } 159 | catch (Exception e) when (_thrownExceptions.IsValueCreated) 160 | { 161 | _thrownExceptions.Value.OnNext(e); 162 | } 163 | } 164 | 165 | /// 166 | /// Raises a property changed event. 167 | /// 168 | /// The name of the property that has changed. 169 | protected virtual void RaisePropertyChanging([CallerMemberName] string propertyName = "") => RaisePropertyChanging(new PropertyChangingEventArgs(propertyName)); 170 | 171 | /// 172 | /// Raises a property changed event. 173 | /// 174 | /// The name of the property that has changed. 175 | protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = "") => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName)); 176 | 177 | /// 178 | /// RaiseAndSetIfChanged fully implements a Setter for a read-write 179 | /// property on a ReactiveObject, using CallerMemberName to raise the notification 180 | /// and the ref to the backing field to set the property. 181 | /// 182 | /// The type of the value. 183 | /// A Reference to the backing field for this 184 | /// property. 185 | /// The new value. 186 | /// The name of the property, usually 187 | /// automatically provided through the CallerMemberName attribute. 188 | protected void RaiseAndSetIfChanged( 189 | ref T backingField, 190 | T newValue, 191 | [CallerMemberName] string? propertyName = null) 192 | { 193 | if (propertyName is null) 194 | { 195 | throw new ArgumentNullException(nameof(propertyName)); 196 | } 197 | 198 | if (EqualityComparer.Default.Equals(backingField, newValue)) 199 | { 200 | return; 201 | } 202 | 203 | RaisePropertyChanging(propertyName); 204 | backingField = newValue; 205 | RaisePropertyChanged(propertyName); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/RxPropertyChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.Mvvm; 6 | 7 | /// 8 | /// IReactivePropertyChangedEventArgs is a generic interface that 9 | /// is used to wrap the NotifyPropertyChangedEventArgs and gives 10 | /// information about changed properties. It includes also 11 | /// the sender of the notification. 12 | /// Note that it is used for both Changing (i.e.'before change') 13 | /// and Changed Observables. 14 | /// 15 | /// The property name. 16 | /// The sender. 17 | /// The sender type. 18 | public record RxPropertyChangedEventArgs(string? PropertyName, TSender Sender) : IRxPropertyEventArgs; -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/RxPropertyChangingEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | namespace ReactiveMarbles.Mvvm; 6 | 7 | /// 8 | /// Event arguments for when a property is changing. 9 | /// 10 | /// The property name. 11 | /// The sender. 12 | /// The sender type. 13 | public record RxPropertyChangingEventArgs(string? PropertyName, TSender Sender) : IRxPropertyEventArgs; -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/RxRecord.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.ComponentModel; 8 | using System.Reactive.Concurrency; 9 | using System.Reactive.Disposables; 10 | using System.Reactive.Linq; 11 | using System.Reactive.Subjects; 12 | using System.Runtime.CompilerServices; 13 | using System.Threading; 14 | 15 | using DynamicData; 16 | 17 | using ReactiveMarbles.Locator; 18 | 19 | namespace ReactiveMarbles.Mvvm; 20 | 21 | /// 22 | /// is the base object for ViewModel classes, and it 23 | /// implements . In addition, provides 24 | /// Changing and Changed Observables to monitor object changes. 25 | /// 26 | public record RxRecord : IRxObject 27 | { 28 | private const string InvalidOperationMessage = "Cannot cast Sender to an IRxObject"; 29 | private readonly Lazy> _thrownExceptions = new( 30 | () => new ProxyScheduledSubject(Scheduler.Immediate, ServiceLocator.Current().GetService().ExceptionHandler), LazyThreadSafetyMode.PublicationOnly); 31 | 32 | private readonly Lazy _notification = new(() => new Notifications()); 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | public RxRecord() 38 | { 39 | Changed = Observable.Create>(observer => 40 | { 41 | void Handler(object? sender, PropertyChangedEventArgs args) => 42 | observer.OnNext(new RxPropertyChangedEventArgs(args.PropertyName, sender as IRxObject ?? throw new InvalidOperationException(InvalidOperationMessage))); 43 | 44 | PropertyChanged += Handler; 45 | return Disposable.Create(() => PropertyChanged -= Handler); 46 | }); 47 | 48 | Changing = Observable.Create>(observer => 49 | { 50 | void Handler(object? sender, PropertyChangingEventArgs args) => 51 | observer.OnNext(new RxPropertyChangingEventArgs(args.PropertyName, sender as IRxObject ?? throw new InvalidOperationException(InvalidOperationMessage))); 52 | 53 | PropertyChanging += Handler; 54 | return Disposable.Create(() => PropertyChanging -= Handler); 55 | }); 56 | } 57 | 58 | /// 59 | public event PropertyChangedEventHandler? PropertyChanged; 60 | 61 | /// 62 | public event PropertyChangingEventHandler? PropertyChanging; 63 | 64 | /// 65 | public IObservable ThrownExceptions => _thrownExceptions.Value; 66 | 67 | /// 68 | public IObservable> Changing { get; } 69 | 70 | /// 71 | public IObservable> Changed { get; } 72 | 73 | /// 74 | public bool AreChangeNotificationsEnabled() => !_notification.IsValueCreated || Interlocked.Read(ref _notification.Value.ChangeNotificationsSuppressed) == 0; 75 | 76 | /// 77 | public bool AreChangeNotificationsDelayed() => _notification.IsValueCreated && Interlocked.Read(ref _notification.Value.ChangeNotificationsDelayed) > 0; 78 | 79 | /// 80 | public IDisposable SuppressChangeNotifications() 81 | { 82 | _ = Interlocked.Increment(ref _notification.Value.ChangeNotificationsSuppressed); 83 | return Disposable.Create(() => Interlocked.Decrement(ref _notification.Value.ChangeNotificationsSuppressed)); 84 | } 85 | 86 | /// 87 | public IDisposable DelayChangeNotifications() 88 | { 89 | _ = Interlocked.Increment(ref _notification.Value.ChangeNotificationsDelayed); 90 | 91 | return Disposable.Create(() => 92 | { 93 | if (Interlocked.Decrement(ref _notification.Value.ChangeNotificationsDelayed) == 0) 94 | { 95 | foreach (var distinctEvent in _notification.Value.PropertyChangingEvents.Items.DistinctEvents()) 96 | { 97 | PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(distinctEvent.PropertyName)); 98 | } 99 | 100 | foreach (var distinctEvent in _notification.Value.PropertyChangedEvents.Items.DistinctEvents()) 101 | { 102 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(distinctEvent.PropertyName)); 103 | } 104 | 105 | _notification.Value.PropertyChangingEvents.Clear(); 106 | _notification.Value.PropertyChangedEvents.Clear(); 107 | } 108 | }); 109 | } 110 | 111 | /// 112 | /// Raises the property changing event. 113 | /// 114 | /// The argument. 115 | protected void RaisePropertyChanging(PropertyChangingEventArgs args) 116 | { 117 | if (!AreChangeNotificationsEnabled()) 118 | { 119 | return; 120 | } 121 | 122 | if (AreChangeNotificationsDelayed()) 123 | { 124 | _notification.Value.PropertyChangingEvents.Add(new RxPropertyChangingEventArgs(args.PropertyName, this)); 125 | return; 126 | } 127 | 128 | try 129 | { 130 | PropertyChanging?.Invoke(this, args); 131 | } 132 | catch (Exception e) when (_thrownExceptions.IsValueCreated) 133 | { 134 | _thrownExceptions.Value.OnNext(e); 135 | } 136 | } 137 | 138 | /// 139 | /// Raises the property changed event. 140 | /// 141 | /// The argument. 142 | protected void RaisePropertyChanged(PropertyChangedEventArgs args) 143 | { 144 | if (!AreChangeNotificationsEnabled()) 145 | { 146 | return; 147 | } 148 | 149 | if (AreChangeNotificationsDelayed()) 150 | { 151 | _notification.Value.PropertyChangedEvents.Add(new RxPropertyChangedEventArgs(args.PropertyName, this)); 152 | return; 153 | } 154 | 155 | try 156 | { 157 | PropertyChanged?.Invoke(this, args); 158 | } 159 | catch (Exception e) when (_thrownExceptions.IsValueCreated) 160 | { 161 | _thrownExceptions.Value.OnNext(e); 162 | } 163 | } 164 | 165 | /// 166 | /// Raises a property changed event. 167 | /// 168 | /// The name of the property that has changed. 169 | protected virtual void RaisePropertyChanging([CallerMemberName] string propertyName = "") => RaisePropertyChanging(new PropertyChangingEventArgs(propertyName)); 170 | 171 | /// 172 | /// Raises a property changed event. 173 | /// 174 | /// The name of the property that has changed. 175 | protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = "") => RaisePropertyChanged(new PropertyChangedEventArgs(propertyName)); 176 | 177 | /// 178 | /// RaiseAndSetIfChanged fully implements a Setter for a read-write 179 | /// property on a ReactiveObject, using CallerMemberName to raise the notification 180 | /// and the ref to the backing field to set the property. 181 | /// 182 | /// The type of the value. 183 | /// A Reference to the backing field for this 184 | /// property. 185 | /// The new value. 186 | /// The name of the property, usually 187 | /// automatically provided through the CallerMemberName attribute. 188 | protected void RaiseAndSetIfChanged( 189 | ref T backingField, 190 | T newValue, 191 | [CallerMemberName] string? propertyName = null) 192 | { 193 | if (propertyName is null) 194 | { 195 | throw new ArgumentNullException(nameof(propertyName)); 196 | } 197 | 198 | if (EqualityComparer.Default.Equals(backingField, newValue)) 199 | { 200 | return; 201 | } 202 | 203 | RaisePropertyChanging(propertyName); 204 | backingField = newValue; 205 | RaisePropertyChanged(propertyName); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/UnhandledErrorException.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | 7 | namespace ReactiveMarbles.Mvvm; 8 | 9 | /// 10 | /// Indicates that an object implementing has caused an error and nothing is attached 11 | /// to to handle that error. 12 | /// 13 | [Serializable] 14 | public class UnhandledErrorException : Exception 15 | { 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | public UnhandledErrorException() 20 | { 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// 27 | /// The exception message. 28 | /// 29 | public UnhandledErrorException(string message) 30 | : base(message) 31 | { 32 | } 33 | 34 | /// 35 | /// Initializes a new instance of the class. 36 | /// 37 | /// 38 | /// The exception message. 39 | /// 40 | /// 41 | /// The exception that caused this exception. 42 | /// 43 | public UnhandledErrorException(string message, Exception innerException) 44 | : base(message, innerException) 45 | { 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ReactiveMarbles.Mvvm/ValueBinder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019-2024 ReactiveUI Association Incorporated. All rights reserved. 2 | // ReactiveUI Association Incorporated licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for full license information. 4 | 5 | using System; 6 | using System.Reactive.Concurrency; 7 | using System.Reactive.Disposables; 8 | using System.Reactive.Linq; 9 | using ReactiveMarbles.Extensions; 10 | 11 | namespace ReactiveMarbles.Mvvm; 12 | 13 | /// 14 | /// is a class to help produce a bindable value from an observable sequence. This property is 15 | /// read-only and will fire change notifications. Using the methods allows easy creation 16 | /// from an observable sequence. 17 | /// 18 | /// The value type. 19 | internal sealed class ValueBinder : IValueBinder 20 | { 21 | private readonly CompositeDisposable _disposable = []; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The source observable. 27 | /// The on changing delegate. 28 | /// The on changed delegate. 29 | /// The thread scheduler. 30 | /// The function that provides an initial value. 31 | public ValueBinder( 32 | IObservable source, 33 | Action? onChanging, 34 | Action onChanged, 35 | IScheduler? scheduler, 36 | Func initialValue) 37 | { 38 | var subject = new ProxyScheduledSubject(scheduler ?? CurrentThreadScheduler.Instance); 39 | Value = initialValue.Invoke(); 40 | _ = subject 41 | .Subscribe(value => 42 | { 43 | onChanging?.Invoke(value); 44 | Value = value; 45 | onChanged.Invoke(value); 46 | }).DisposeWith(_disposable); 47 | _ = source.DistinctUntilChanged().StartWith(Value).Subscribe(subject).DisposeWith(_disposable); 48 | } 49 | 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | /// The source observable. 54 | /// The on changed delegate. 55 | /// The function that provides an initial value. 56 | public ValueBinder(IObservable source, Action onChanged, Func initialValue) 57 | : this(source, null, onChanged, null, initialValue) 58 | { 59 | } 60 | 61 | /// 62 | /// Initializes a new instance of the class. 63 | /// 64 | /// The source observable. 65 | /// The on changing delegate. 66 | /// The on changed delegate. 67 | /// The function that provides an initial value. 68 | public ValueBinder(IObservable source, Action onChanging, Action onChanged, Func initialValue) 69 | : this(source, onChanging, onChanged, null, initialValue) 70 | { 71 | } 72 | 73 | /// 74 | /// Initializes a new instance of the class. 75 | /// 76 | /// The source observable. 77 | /// The on changed. 78 | /// The scheduler. 79 | /// The function that provides an initial value. 80 | public ValueBinder(IObservable source, Action onChanged, IScheduler scheduler, Func initialValue) 81 | : this(source, null, onChanged, scheduler, initialValue) 82 | { 83 | } 84 | 85 | /// 86 | /// Gets the latest value. 87 | /// 88 | public T Value { get; private set; } 89 | 90 | /// 91 | public void Dispose() => _disposable.Dispose(); 92 | } 93 | -------------------------------------------------------------------------------- /src/directory.build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | ReactiveUI Association Inc 4 | ReactiveUI Association Inc 5 | Copyright (c) ReactiveUI Association Inc © $([System.DateTime]::Now.ToString('yyyy')) 6 | MIT 7 | https://github.com/reactivemarbles/Mvvm 8 | Common base classes for the MVVM pattern for Reactive Marbles. 9 | logo.png 10 | docs\README.md 11 | glennawatson;rlittlesii;Chris Pulman 12 | system.reactive;propertychanged;inpc;reactive;functional;mvvm 13 | https://github.com/reactivemarbles/Mvvm/releases 14 | https://github.com/reactivemarbles/Mvvm 15 | git 16 | true 17 | enable 18 | true 19 | true 20 | AnyCPU 21 | $(MSBuildProjectName.Contains('Tests')) 22 | $(MSBuildProjectName.Contains('Benchmarks')) 23 | embedded 24 | true 25 | 26 | true 27 | 28 | true 29 | 30 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 31 | 4 32 | true 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | $(MSBuildThisFileDirectory) 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | all 52 | runtime; build; native; contentfiles; analyzers 53 | 54 | 55 | 56 | 57 | 58 | all 59 | runtime; build; native; contentfiles; analyzers 60 | 61 | 62 | all 63 | runtime; build; native; contentfiles; analyzers 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /src/directory.build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(AssemblyName) ($(TargetFramework)) 6 | False 7 | false 8 | 9 | 10 | 11 | $(DefineConstants);NETSTANDARD;PORTABLE 12 | 13 | 14 | $(DefineConstants);NET_461;XAML 15 | 16 | 17 | $(DefineConstants);NETFX_CORE;XAML;WINDOWS;WINDOWS_UWP 18 | 19 | 20 | $(DefineConstants);MONO;UIKIT;COCOA;IOS 21 | 22 | 23 | $(DefineConstants);MONO;COCOA;MAC 24 | 25 | 26 | $(DefineConstants);MONO;UIKIT;COCOA;TVOS 27 | 28 | 29 | $(DefineConstants);MONO;COCOA;WATCHOS 30 | 31 | 32 | $(DefineConstants);MONO;ANDROID 33 | false 34 | 35 | 36 | $(DefineConstants);NETCOREAPP 37 | 38 | 39 | $(DefineConstants);TIZEN 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/directory.packages.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "indentation": { 5 | "useTabs": false, 6 | "indentationSize": 4 7 | }, 8 | "documentationRules": { 9 | "documentExposedElements": true, 10 | "documentInternalElements": false, 11 | "documentPrivateElements": false, 12 | "documentInterfaces": true, 13 | "documentPrivateFields": false, 14 | "documentationCulture": "en-US", 15 | "companyName": "ReactiveUI Association Incorporated", 16 | "copyrightText": "Copyright (c) 2019-2024 {companyName}. All rights reserved.\n{companyName} licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.", 17 | "variables": { 18 | "licenseName": "MIT", 19 | "licenseFile": "LICENSE" 20 | }, 21 | "xmlHeader": false 22 | }, 23 | "layoutRules": { 24 | "newlineAtEndOfFile": "allow", 25 | "allowConsecutiveUsings": true 26 | }, 27 | "maintainabilityRules": { 28 | "topLevelTypes": [ 29 | "class", 30 | "interface", 31 | "struct", 32 | "enum", 33 | "delegate" 34 | ] 35 | }, 36 | "orderingRules": { 37 | "usingDirectivesPlacement": "outsideNamespace", 38 | "systemUsingDirectivesFirst": true 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /version.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", 3 | "version": "1.0", 4 | "publicReleaseRefSpec": [ 5 | "^refs/heads/master$", // we release out of master 6 | "^refs/heads/main$", 7 | "^refs/heads/latest$", 8 | "^refs/heads/preview/.*", // we release previews 9 | "^refs/heads/rel/\\d+\\.\\d+\\.\\d+" // we also release branches starting with rel/N.N.N 10 | ], 11 | "nugetPackageVersion":{ 12 | "semVer": 2 13 | }, 14 | "cloudBuild": { 15 | "setVersionVariables": true, 16 | "buildNumber": { 17 | "enabled": false 18 | } 19 | } 20 | } 21 | --------------------------------------------------------------------------------