From ade98942fcad7334254dfaf460c5d38e3ac44ab9 Mon Sep 17 00:00:00 2001
From: rlittlesii <6969701+RLittlesII@users.noreply.github.com>
Date: Mon, 18 Apr 2022 20:22:08 -0500
Subject: [PATCH 1/3] feature: Add RxCommand
Co-Authored-By: glennawatson <5834289+glennawatson@users.noreply.github.com>
---
.editorconfig | 457 ++++++
.gitattributes | 289 ++++
images/logo.png | 3 +
.../FakeCommand.cs | 63 +
.../ICommandHolder.cs | 25 +
.../ReactiveMarbles.Command.Tests.csproj | 31 +
.../RxCommandHolder.cs | 26 +
.../RxCommandTests.cs | 1315 +++++++++++++++++
src/ReactiveMarbles.Command.sln | 22 +
src/ReactiveMarbles.Command/IRxCommand.cs | 114 ++
.../ObservableConstants.cs | 19 +
.../ReactiveMarbles.Command.csproj | 18 +
src/ReactiveMarbles.Command/RxCommand.cs | 837 +++++++++++
.../RxCommandExtensions.cs | 221 +++
src/directory.build.props | 65 +
src/directory.build.targets | 44 +
src/directory.packages.props | 47 +
src/stylecop.json | 41 +
version.json | 20 +
19 files changed, 3657 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .gitattributes
create mode 100644 images/logo.png
create mode 100644 src/ReactiveMarbles.Command.Tests/FakeCommand.cs
create mode 100644 src/ReactiveMarbles.Command.Tests/ICommandHolder.cs
create mode 100644 src/ReactiveMarbles.Command.Tests/ReactiveMarbles.Command.Tests.csproj
create mode 100644 src/ReactiveMarbles.Command.Tests/RxCommandHolder.cs
create mode 100644 src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
create mode 100644 src/ReactiveMarbles.Command.sln
create mode 100644 src/ReactiveMarbles.Command/IRxCommand.cs
create mode 100644 src/ReactiveMarbles.Command/ObservableConstants.cs
create mode 100644 src/ReactiveMarbles.Command/ReactiveMarbles.Command.csproj
create mode 100644 src/ReactiveMarbles.Command/RxCommand.cs
create mode 100644 src/ReactiveMarbles.Command/RxCommandExtensions.cs
create mode 100644 src/directory.build.props
create mode 100644 src/directory.build.targets
create mode 100644 src/directory.packages.props
create mode 100644 src/stylecop.json
create mode 100644 version.json
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..ff8681f
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,457 @@
+# editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Default settings:
+# A newline ending every file
+# Use 4 spaces as indentation
+[*]
+insert_final_newline = true
+indent_style = space
+indent_size = 4
+
+[project.json]
+indent_size = 2
+
+# C# files
+[*.cs]
+# New line preferences
+csharp_new_line_before_open_brace = all
+csharp_new_line_before_else = true
+csharp_new_line_before_catch = true
+csharp_new_line_before_finally = true
+csharp_new_line_before_members_in_object_initializers = true
+csharp_new_line_before_members_in_anonymous_types = true
+csharp_new_line_between_query_expression_clauses = true
+
+# Indentation preferences
+csharp_indent_block_contents = true
+csharp_indent_braces = false
+csharp_indent_case_contents = true
+csharp_indent_case_contents_when_block = true
+csharp_indent_switch_labels = true
+csharp_indent_labels = one_less_than_current
+
+# Modifier preferences
+csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
+
+# avoid this. unless absolutely necessary
+dotnet_style_qualification_for_field = false:suggestion
+dotnet_style_qualification_for_property = false:suggestion
+dotnet_style_qualification_for_method = false:suggestion
+dotnet_style_qualification_for_event = false:suggestion
+
+# only use var when it's obvious what the variable type is
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
+
+# prefer C# premade types.
+dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
+dotnet_style_predefined_type_for_member_access = true:suggestion
+
+# name all constant fields using PascalCase
+dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
+dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
+dotnet_naming_symbols.constant_fields.applicable_kinds = field
+dotnet_naming_symbols.constant_fields.required_modifiers = const
+dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+
+# static fields should have _ prefix
+dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
+dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
+dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
+dotnet_naming_symbols.static_fields.applicable_kinds = field
+dotnet_naming_symbols.static_fields.required_modifiers = static
+dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
+dotnet_naming_style.static_prefix_style.required_prefix = _
+dotnet_naming_style.static_prefix_style.capitalization = camel_case
+
+# internal and private fields should be _camelCase
+dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
+dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
+dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
+dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
+dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
+dotnet_naming_style.camel_case_underscore_style.required_prefix = _
+dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
+
+# Code style defaults
+csharp_using_directive_placement = outside_namespace:suggestion
+dotnet_sort_system_directives_first = true
+csharp_prefer_braces = true:silent
+csharp_preserve_single_line_blocks = true:none
+csharp_preserve_single_line_statements = false:none
+csharp_prefer_static_local_function = true:suggestion
+csharp_prefer_simple_using_statement = false:none
+csharp_style_prefer_switch_expression = true:suggestion
+
+# Code quality
+dotnet_style_readonly_field = true:suggestion
+dotnet_code_quality_unused_parameters = non_public:suggestion
+
+# Expression-level preferences
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:silent
+dotnet_style_prefer_conditional_expression_over_return = true:silent
+csharp_prefer_simple_default_expression = true:suggestion
+
+# Expression-bodied members
+csharp_style_expression_bodied_methods = true:suggestion
+csharp_style_expression_bodied_constructors = true:suggestion
+csharp_style_expression_bodied_operators = true:suggestion
+csharp_style_expression_bodied_properties = true:suggestion
+csharp_style_expression_bodied_indexers = true:suggestion
+csharp_style_expression_bodied_accessors = true:suggestion
+csharp_style_expression_bodied_lambdas = true:suggestion
+csharp_style_expression_bodied_local_functions = true:suggestion
+
+# Pattern matching
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+
+# Null checking preferences
+csharp_style_throw_expression = true:suggestion
+csharp_style_conditional_delegate_call = true:suggestion
+
+# Other features
+csharp_style_prefer_index_operator = false:none
+csharp_style_prefer_range_operator = false:none
+csharp_style_pattern_local_over_anonymous_function = false:none
+
+# Space preferences
+csharp_space_after_cast = false
+csharp_space_after_colon_in_inheritance_clause = true
+csharp_space_after_comma = true
+csharp_space_after_dot = false
+csharp_space_after_keywords_in_control_flow_statements = true
+csharp_space_after_semicolon_in_for_statement = true
+csharp_space_around_binary_operators = before_and_after
+csharp_space_around_declaration_statements = do_not_ignore
+csharp_space_before_colon_in_inheritance_clause = true
+csharp_space_before_comma = false
+csharp_space_before_dot = false
+csharp_space_before_open_square_brackets = false
+csharp_space_before_semicolon_in_for_statement = false
+csharp_space_between_empty_square_brackets = false
+csharp_space_between_method_call_empty_parameter_list_parentheses = false
+csharp_space_between_method_call_name_and_opening_parenthesis = false
+csharp_space_between_method_call_parameter_list_parentheses = false
+csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
+csharp_space_between_method_declaration_name_and_open_parenthesis = false
+csharp_space_between_method_declaration_parameter_list_parentheses = false
+csharp_space_between_parentheses = false
+csharp_space_between_square_brackets = false
+
+# analyzers
+dotnet_diagnostic.AvoidAsyncVoid.severity = suggestion
+
+dotnet_diagnostic.CA1000.severity = none
+dotnet_diagnostic.CA1001.severity = error
+dotnet_diagnostic.CA1009.severity = error
+dotnet_diagnostic.CA1016.severity = error
+dotnet_diagnostic.CA1030.severity = none
+dotnet_diagnostic.CA1031.severity = none
+dotnet_diagnostic.CA1033.severity = none
+dotnet_diagnostic.CA1036.severity = none
+dotnet_diagnostic.CA1049.severity = error
+dotnet_diagnostic.CA1056.severity = suggestion
+dotnet_diagnostic.CA1060.severity = error
+dotnet_diagnostic.CA1061.severity = error
+dotnet_diagnostic.CA1063.severity = error
+dotnet_diagnostic.CA1065.severity = error
+dotnet_diagnostic.CA1301.severity = error
+dotnet_diagnostic.CA1303.severity = none
+dotnet_diagnostic.CA1308.severity = none
+dotnet_diagnostic.CA1400.severity = error
+dotnet_diagnostic.CA1401.severity = error
+dotnet_diagnostic.CA1403.severity = error
+dotnet_diagnostic.CA1404.severity = error
+dotnet_diagnostic.CA1405.severity = error
+dotnet_diagnostic.CA1410.severity = error
+dotnet_diagnostic.CA1415.severity = error
+dotnet_diagnostic.CA1507.severity = error
+dotnet_diagnostic.CA1710.severity = suggestion
+dotnet_diagnostic.CA1724.severity = none
+dotnet_diagnostic.CA1810.severity = none
+dotnet_diagnostic.CA1821.severity = error
+dotnet_diagnostic.CA1900.severity = error
+dotnet_diagnostic.CA1901.severity = error
+dotnet_diagnostic.CA2000.severity = none
+dotnet_diagnostic.CA2002.severity = error
+dotnet_diagnostic.CA2007.severity = none
+dotnet_diagnostic.CA2100.severity = error
+dotnet_diagnostic.CA2101.severity = error
+dotnet_diagnostic.CA2108.severity = error
+dotnet_diagnostic.CA2111.severity = error
+dotnet_diagnostic.CA2112.severity = error
+dotnet_diagnostic.CA2114.severity = error
+dotnet_diagnostic.CA2116.severity = error
+dotnet_diagnostic.CA2117.severity = error
+dotnet_diagnostic.CA2122.severity = error
+dotnet_diagnostic.CA2123.severity = error
+dotnet_diagnostic.CA2124.severity = error
+dotnet_diagnostic.CA2126.severity = error
+dotnet_diagnostic.CA2131.severity = error
+dotnet_diagnostic.CA2132.severity = error
+dotnet_diagnostic.CA2133.severity = error
+dotnet_diagnostic.CA2134.severity = error
+dotnet_diagnostic.CA2137.severity = error
+dotnet_diagnostic.CA2138.severity = error
+dotnet_diagnostic.CA2140.severity = error
+dotnet_diagnostic.CA2141.severity = error
+dotnet_diagnostic.CA2146.severity = error
+dotnet_diagnostic.CA2147.severity = error
+dotnet_diagnostic.CA2149.severity = error
+dotnet_diagnostic.CA2200.severity = error
+dotnet_diagnostic.CA2202.severity = error
+dotnet_diagnostic.CA2207.severity = error
+dotnet_diagnostic.CA2212.severity = error
+dotnet_diagnostic.CA2213.severity = error
+dotnet_diagnostic.CA2214.severity = error
+dotnet_diagnostic.CA2216.severity = error
+dotnet_diagnostic.CA2220.severity = error
+dotnet_diagnostic.CA2229.severity = error
+dotnet_diagnostic.CA2231.severity = error
+dotnet_diagnostic.CA2232.severity = error
+dotnet_diagnostic.CA2235.severity = error
+dotnet_diagnostic.CA2236.severity = error
+dotnet_diagnostic.CA2237.severity = error
+dotnet_diagnostic.CA2238.severity = error
+dotnet_diagnostic.CA2240.severity = error
+dotnet_diagnostic.CA2241.severity = error
+dotnet_diagnostic.CA2242.severity = error
+
+dotnet_diagnostic.RCS1001.severity = error
+dotnet_diagnostic.RCS1018.severity = error
+dotnet_diagnostic.RCS1037.severity = error
+dotnet_diagnostic.RCS1055.severity = error
+dotnet_diagnostic.RCS1062.severity = error
+dotnet_diagnostic.RCS1066.severity = error
+dotnet_diagnostic.RCS1069.severity = error
+dotnet_diagnostic.RCS1071.severity = error
+dotnet_diagnostic.RCS1074.severity = error
+dotnet_diagnostic.RCS1090.severity = error
+dotnet_diagnostic.RCS1138.severity = error
+dotnet_diagnostic.RCS1139.severity = error
+dotnet_diagnostic.RCS1163.severity = suggestion
+dotnet_diagnostic.RCS1168.severity = suggestion
+dotnet_diagnostic.RCS1188.severity = error
+dotnet_diagnostic.RCS1201.severity = error
+dotnet_diagnostic.RCS1207.severity = error
+dotnet_diagnostic.RCS1211.severity = error
+dotnet_diagnostic.RCS1507.severity = error
+
+dotnet_diagnostic.SA1000.severity = error
+dotnet_diagnostic.SA1001.severity = error
+dotnet_diagnostic.SA1002.severity = error
+dotnet_diagnostic.SA1003.severity = error
+dotnet_diagnostic.SA1004.severity = error
+dotnet_diagnostic.SA1005.severity = error
+dotnet_diagnostic.SA1006.severity = error
+dotnet_diagnostic.SA1007.severity = error
+dotnet_diagnostic.SA1008.severity = error
+dotnet_diagnostic.SA1009.severity = error
+dotnet_diagnostic.SA1010.severity = error
+dotnet_diagnostic.SA1011.severity = error
+dotnet_diagnostic.SA1012.severity = error
+dotnet_diagnostic.SA1013.severity = error
+dotnet_diagnostic.SA1014.severity = error
+dotnet_diagnostic.SA1015.severity = error
+dotnet_diagnostic.SA1016.severity = error
+dotnet_diagnostic.SA1017.severity = error
+dotnet_diagnostic.SA1018.severity = error
+dotnet_diagnostic.SA1019.severity = error
+dotnet_diagnostic.SA1020.severity = error
+dotnet_diagnostic.SA1021.severity = error
+dotnet_diagnostic.SA1022.severity = error
+dotnet_diagnostic.SA1023.severity = error
+dotnet_diagnostic.SA1024.severity = error
+dotnet_diagnostic.SA1025.severity = error
+dotnet_diagnostic.SA1026.severity = error
+dotnet_diagnostic.SA1027.severity = error
+dotnet_diagnostic.SA1028.severity = error
+dotnet_diagnostic.SA1100.severity = error
+dotnet_diagnostic.SA1101.severity = none
+dotnet_diagnostic.SA1102.severity = error
+dotnet_diagnostic.SA1103.severity = error
+dotnet_diagnostic.SA1104.severity = error
+dotnet_diagnostic.SA1105.severity = error
+dotnet_diagnostic.SA1106.severity = error
+dotnet_diagnostic.SA1107.severity = error
+dotnet_diagnostic.SA1108.severity = error
+dotnet_diagnostic.SA1110.severity = error
+dotnet_diagnostic.SA1111.severity = error
+dotnet_diagnostic.SA1112.severity = error
+dotnet_diagnostic.SA1113.severity = error
+dotnet_diagnostic.SA1114.severity = error
+dotnet_diagnostic.SA1115.severity = error
+dotnet_diagnostic.SA1116.severity = error
+dotnet_diagnostic.SA1117.severity = error
+dotnet_diagnostic.SA1118.severity = error
+dotnet_diagnostic.SA1119.severity = error
+dotnet_diagnostic.SA1120.severity = error
+dotnet_diagnostic.SA1121.severity = error
+dotnet_diagnostic.SA1122.severity = error
+dotnet_diagnostic.SA1123.severity = error
+dotnet_diagnostic.SA1124.severity = error
+dotnet_diagnostic.SA1125.severity = error
+dotnet_diagnostic.SA1127.severity = error
+dotnet_diagnostic.SA1128.severity = error
+dotnet_diagnostic.SA1129.severity = error
+dotnet_diagnostic.SA1130.severity = error
+dotnet_diagnostic.SA1131.severity = error
+dotnet_diagnostic.SA1132.severity = error
+dotnet_diagnostic.SA1133.severity = error
+dotnet_diagnostic.SA1134.severity = error
+dotnet_diagnostic.SA1135.severity = error
+dotnet_diagnostic.SA1136.severity = error
+dotnet_diagnostic.SA1137.severity = error
+dotnet_diagnostic.SA1139.severity = error
+dotnet_diagnostic.SA1200.severity = none
+dotnet_diagnostic.SA1201.severity = error
+dotnet_diagnostic.SA1202.severity = error
+dotnet_diagnostic.SA1203.severity = error
+dotnet_diagnostic.SA1204.severity = error
+dotnet_diagnostic.SA1205.severity = error
+dotnet_diagnostic.SA1206.severity = error
+dotnet_diagnostic.SA1207.severity = error
+dotnet_diagnostic.SA1208.severity = error
+dotnet_diagnostic.SA1209.severity = error
+dotnet_diagnostic.SA1210.severity = error
+dotnet_diagnostic.SA1211.severity = error
+dotnet_diagnostic.SA1212.severity = error
+dotnet_diagnostic.SA1213.severity = error
+dotnet_diagnostic.SA1214.severity = error
+dotnet_diagnostic.SA1216.severity = error
+dotnet_diagnostic.SA1217.severity = error
+dotnet_diagnostic.SA1300.severity = error
+dotnet_diagnostic.SA1302.severity = error
+dotnet_diagnostic.SA1303.severity = error
+dotnet_diagnostic.SA1304.severity = error
+dotnet_diagnostic.SA1306.severity = none
+dotnet_diagnostic.SA1307.severity = error
+dotnet_diagnostic.SA1308.severity = error
+dotnet_diagnostic.SA1309.severity = none
+dotnet_diagnostic.SA1310.severity = error
+dotnet_diagnostic.SA1311.severity = none
+dotnet_diagnostic.SA1312.severity = error
+dotnet_diagnostic.SA1313.severity = error
+dotnet_diagnostic.SA1314.severity = error
+dotnet_diagnostic.SA1316.severity = none
+dotnet_diagnostic.SA1400.severity = error
+dotnet_diagnostic.SA1401.severity = error
+dotnet_diagnostic.SA1402.severity = error
+dotnet_diagnostic.SA1403.severity = error
+dotnet_diagnostic.SA1404.severity = error
+dotnet_diagnostic.SA1405.severity = error
+dotnet_diagnostic.SA1406.severity = error
+dotnet_diagnostic.SA1407.severity = error
+dotnet_diagnostic.SA1408.severity = error
+dotnet_diagnostic.SA1410.severity = error
+dotnet_diagnostic.SA1411.severity = error
+dotnet_diagnostic.SA1413.severity = none
+dotnet_diagnostic.SA1500.severity = error
+dotnet_diagnostic.SA1501.severity = error
+dotnet_diagnostic.SA1502.severity = error
+dotnet_diagnostic.SA1503.severity = error
+dotnet_diagnostic.SA1504.severity = error
+dotnet_diagnostic.SA1505.severity = error
+dotnet_diagnostic.SA1506.severity = error
+dotnet_diagnostic.SA1507.severity = error
+dotnet_diagnostic.SA1508.severity = error
+dotnet_diagnostic.SA1509.severity = error
+dotnet_diagnostic.SA1510.severity = error
+dotnet_diagnostic.SA1511.severity = error
+dotnet_diagnostic.SA1512.severity = error
+dotnet_diagnostic.SA1513.severity = error
+dotnet_diagnostic.SA1514.severity = error
+dotnet_diagnostic.SA1515.severity = error
+dotnet_diagnostic.SA1516.severity = error
+dotnet_diagnostic.SA1517.severity = error
+dotnet_diagnostic.SA1518.severity = error
+dotnet_diagnostic.SA1519.severity = error
+dotnet_diagnostic.SA1520.severity = error
+dotnet_diagnostic.SA1600.severity = error
+dotnet_diagnostic.SA1601.severity = error
+dotnet_diagnostic.SA1602.severity = error
+dotnet_diagnostic.SA1604.severity = error
+dotnet_diagnostic.SA1605.severity = error
+dotnet_diagnostic.SA1606.severity = error
+dotnet_diagnostic.SA1607.severity = error
+dotnet_diagnostic.SA1608.severity = error
+dotnet_diagnostic.SA1610.severity = error
+dotnet_diagnostic.SA1611.severity = error
+dotnet_diagnostic.SA1612.severity = error
+dotnet_diagnostic.SA1613.severity = error
+dotnet_diagnostic.SA1614.severity = error
+dotnet_diagnostic.SA1615.severity = error
+dotnet_diagnostic.SA1616.severity = error
+dotnet_diagnostic.SA1617.severity = error
+dotnet_diagnostic.SA1618.severity = error
+dotnet_diagnostic.SA1619.severity = error
+dotnet_diagnostic.SA1620.severity = error
+dotnet_diagnostic.SA1621.severity = error
+dotnet_diagnostic.SA1622.severity = error
+dotnet_diagnostic.SA1623.severity = error
+dotnet_diagnostic.SA1624.severity = error
+dotnet_diagnostic.SA1625.severity = error
+dotnet_diagnostic.SA1626.severity = error
+dotnet_diagnostic.SA1627.severity = error
+dotnet_diagnostic.SA1629.severity = error
+dotnet_diagnostic.SA1633.severity = error
+dotnet_diagnostic.SA1634.severity = error
+dotnet_diagnostic.SA1635.severity = error
+dotnet_diagnostic.SA1636.severity = error
+dotnet_diagnostic.SA1637.severity = none
+dotnet_diagnostic.SA1638.severity = none
+dotnet_diagnostic.SA1640.severity = error
+dotnet_diagnostic.SA1641.severity = error
+dotnet_diagnostic.SA1642.severity = error
+dotnet_diagnostic.SA1643.severity = error
+dotnet_diagnostic.SA1649.severity = error
+dotnet_diagnostic.SA1651.severity = error
+
+dotnet_diagnostic.SX1101.severity = error
+dotnet_diagnostic.SX1309.severity = error
+dotnet_diagnostic.SX1623.severity = none
+
+dotnet_diagnostic.AD0001.severity = none
+
+# C++ Files
+[*.{cpp,h,in}]
+curly_bracket_next_line = true
+indent_brace_style = Allman
+
+# Xml project files
+[*.{csproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
+indent_size = 2
+
+# Xml build files
+[*.builds]
+indent_size = 2
+
+# Xml files
+[*.{xml,stylecop,resx,ruleset}]
+indent_size = 2
+
+# Xml config files
+[*.{props,targets,config,nuspec}]
+indent_size = 2
+
+# Shell scripts
+[*.sh]
+end_of_line = lf
+[*.{cmd, bat}]
+end_of_line = crlf
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fdce30d
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,289 @@
+# Catch all for anything we forgot. Add rules if you get CRLF to LF warnings.
+* text=auto
+
+# Text files that should be normalized to LF in odb.
+*.cs text diff=csharp
+*.xaml text
+*.config text
+*.c text
+*.h text
+*.cpp text
+*.hpp text
+*.sln text
+*.csproj text
+*.vcxproj text
+*.md text
+*.tt text
+*.sh text
+*.ps1 text
+*.cmd text
+*.bat text
+*.markdown text
+*.msbuild text
+# Binary files that should not be normalized or diffed
+*.png binary
+*.jpg binary
+*.gif binary
+*.ico binary
+*.rc binary
+*.pfx binary
+*.snk binary
+*.dll binary
+*.exe binary
+*.lib binary
+*.exp binary
+*.pdb binary
+*.sdf binary
+*.7z binary
+# Generated file should just use CRLF, it's fiiine
+SolutionInfo.cs text eol=crlf diff=csharp
+*.mht filter=lfs diff=lfs merge=lfs -text
+*.ppam filter=lfs diff=lfs merge=lfs -text
+*.wmv filter=lfs diff=lfs merge=lfs -text
+*.btif filter=lfs diff=lfs merge=lfs -text
+*.fla filter=lfs diff=lfs merge=lfs -text
+*.qt filter=lfs diff=lfs merge=lfs -text
+*.xlam filter=lfs diff=lfs merge=lfs -text
+*.xm filter=lfs diff=lfs merge=lfs -text
+*.djvu filter=lfs diff=lfs merge=lfs -text
+*.woff filter=lfs diff=lfs merge=lfs -text
+*.a filter=lfs diff=lfs merge=lfs -text
+*.bak filter=lfs diff=lfs merge=lfs -text
+*.lha filter=lfs diff=lfs merge=lfs -text
+*.mpg filter=lfs diff=lfs merge=lfs -text
+*.xltm filter=lfs diff=lfs merge=lfs -text
+*.eol filter=lfs diff=lfs merge=lfs -text
+*.ipa filter=lfs diff=lfs merge=lfs -text
+*.ttf filter=lfs diff=lfs merge=lfs -text
+*.uvm filter=lfs diff=lfs merge=lfs -text
+*.cmx filter=lfs diff=lfs merge=lfs -text
+*.dng filter=lfs diff=lfs merge=lfs -text
+*.xltx filter=lfs diff=lfs merge=lfs -text
+*.fli filter=lfs diff=lfs merge=lfs -text
+*.wmx filter=lfs diff=lfs merge=lfs -text
+*.jxr filter=lfs diff=lfs merge=lfs -text
+*.pyv filter=lfs diff=lfs merge=lfs -text
+*.s7z filter=lfs diff=lfs merge=lfs -text
+*.csv filter=lfs diff=lfs merge=lfs -text
+*.pptm filter=lfs diff=lfs merge=lfs -text
+*.rz filter=lfs diff=lfs merge=lfs -text
+*.wm filter=lfs diff=lfs merge=lfs -text
+*.xlsx filter=lfs diff=lfs merge=lfs -text
+*.bh filter=lfs diff=lfs merge=lfs -text
+*.dat filter=lfs diff=lfs merge=lfs -text
+*.mid filter=lfs diff=lfs merge=lfs -text
+*.mpga filter=lfs diff=lfs merge=lfs -text
+*.ogg filter=lfs diff=lfs merge=lfs -text
+*.s3m filter=lfs diff=lfs merge=lfs -text
+*.mar filter=lfs diff=lfs merge=lfs -text
+*.movie filter=lfs diff=lfs merge=lfs -text
+*.pptx filter=lfs diff=lfs merge=lfs -text
+*.dll filter=lfs diff=lfs merge=lfs -text
+*.docm filter=lfs diff=lfs merge=lfs -text
+*.m3u filter=lfs diff=lfs merge=lfs -text
+*.mov filter=lfs diff=lfs merge=lfs -text
+*.aac filter=lfs diff=lfs merge=lfs -text
+*.jar filter=lfs diff=lfs merge=lfs -text
+*.midi filter=lfs diff=lfs merge=lfs -text
+*.mobi filter=lfs diff=lfs merge=lfs -text
+*.potm filter=lfs diff=lfs merge=lfs -text
+*.woff2 filter=lfs diff=lfs merge=lfs -text
+*.cab filter=lfs diff=lfs merge=lfs -text
+*.dmg filter=lfs diff=lfs merge=lfs -text
+*.pdf filter=lfs diff=lfs merge=lfs -text
+*.war filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.icns filter=lfs diff=lfs merge=lfs -text
+*.slk filter=lfs diff=lfs merge=lfs -text
+*.wbmp filter=lfs diff=lfs merge=lfs -text
+*.xpm filter=lfs diff=lfs merge=lfs -text
+*.xmind filter=lfs diff=lfs merge=lfs -text
+*.3g2 filter=lfs diff=lfs merge=lfs -text
+*.m4v filter=lfs diff=lfs merge=lfs -text
+*.pic filter=lfs diff=lfs merge=lfs -text
+*.uvi filter=lfs diff=lfs merge=lfs -text
+*.uvp filter=lfs diff=lfs merge=lfs -text
+*.xls filter=lfs diff=lfs merge=lfs -text
+*.jpgv filter=lfs diff=lfs merge=lfs -text
+*.mka filter=lfs diff=lfs merge=lfs -text
+*.swf filter=lfs diff=lfs merge=lfs -text
+*.uvs filter=lfs diff=lfs merge=lfs -text
+*.wav filter=lfs diff=lfs merge=lfs -text
+*.ecelp4800 filter=lfs diff=lfs merge=lfs -text
+*.mng filter=lfs diff=lfs merge=lfs -text
+*.pps filter=lfs diff=lfs merge=lfs -text
+*.whl filter=lfs diff=lfs merge=lfs -text
+*.arj filter=lfs diff=lfs merge=lfs -text
+*.lzh filter=lfs diff=lfs merge=lfs -text
+*.raw filter=lfs diff=lfs merge=lfs -text
+*.rlc filter=lfs diff=lfs merge=lfs -text
+*.sgi filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.au filter=lfs diff=lfs merge=lfs -text
+*.dcm filter=lfs diff=lfs merge=lfs -text
+*.GIF filter=lfs diff=lfs merge=lfs -text
+*.resources filter=lfs diff=lfs merge=lfs -text
+*.txz filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.sil filter=lfs diff=lfs merge=lfs -text
+*.bk filter=lfs diff=lfs merge=lfs -text
+*.DS_Store filter=lfs diff=lfs merge=lfs -text
+*.ief filter=lfs diff=lfs merge=lfs -text
+*.JPEG filter=lfs diff=lfs merge=lfs -text
+*.pbm filter=lfs diff=lfs merge=lfs -text
+*.png filter=lfs diff=lfs merge=lfs -text
+*.sketch filter=lfs diff=lfs merge=lfs -text
+*.tbz2 filter=lfs diff=lfs merge=lfs -text
+*.nef filter=lfs diff=lfs merge=lfs -text
+*.oga filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.ecelp7470 filter=lfs diff=lfs merge=lfs -text
+*.xlt filter=lfs diff=lfs merge=lfs -text
+*.exe filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text
+*.pnm filter=lfs diff=lfs merge=lfs -text
+*.ttc filter=lfs diff=lfs merge=lfs -text
+*.wdp filter=lfs diff=lfs merge=lfs -text
+*.xbm filter=lfs diff=lfs merge=lfs -text
+*.ecelp9600 filter=lfs diff=lfs merge=lfs -text
+*.pot filter=lfs diff=lfs merge=lfs -text
+*.wvx filter=lfs diff=lfs merge=lfs -text
+*.uvu filter=lfs diff=lfs merge=lfs -text
+*.asf filter=lfs diff=lfs merge=lfs -text
+*.dxf filter=lfs diff=lfs merge=lfs -text
+*.flv filter=lfs diff=lfs merge=lfs -text
+*.mdi filter=lfs diff=lfs merge=lfs -text
+*.pcx filter=lfs diff=lfs merge=lfs -text
+*.tiff filter=lfs diff=lfs merge=lfs -text
+*.bzip2 filter=lfs diff=lfs merge=lfs -text
+*.deb filter=lfs diff=lfs merge=lfs -text
+*.graffle filter=lfs diff=lfs merge=lfs -text
+*.h261 filter=lfs diff=lfs merge=lfs -text
+*.jpeg filter=lfs diff=lfs merge=lfs -text
+*.ppm filter=lfs diff=lfs merge=lfs -text
+*.tif filter=lfs diff=lfs merge=lfs -text
+*.ppt filter=lfs diff=lfs merge=lfs -text
+*.fbs filter=lfs diff=lfs merge=lfs -text
+*.gzip filter=lfs diff=lfs merge=lfs -text
+*.o filter=lfs diff=lfs merge=lfs -text
+*.sub filter=lfs diff=lfs merge=lfs -text
+*.z filter=lfs diff=lfs merge=lfs -text
+*.alz filter=lfs diff=lfs merge=lfs -text
+*.BMP filter=lfs diff=lfs merge=lfs -text
+*.dotm filter=lfs diff=lfs merge=lfs -text
+*.key filter=lfs diff=lfs merge=lfs -text
+*.rgb filter=lfs diff=lfs merge=lfs -text
+*.f4v filter=lfs diff=lfs merge=lfs -text
+*.iso filter=lfs diff=lfs merge=lfs -text
+*.ai filter=lfs diff=lfs merge=lfs -text
+*.dtshd filter=lfs diff=lfs merge=lfs -text
+*.fpx filter=lfs diff=lfs merge=lfs -text
+*.shar filter=lfs diff=lfs merge=lfs -text
+*.img filter=lfs diff=lfs merge=lfs -text
+*.rmf filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.eot filter=lfs diff=lfs merge=lfs -text
+*.wma filter=lfs diff=lfs merge=lfs -text
+*.cpio filter=lfs diff=lfs merge=lfs -text
+*.cr2 filter=lfs diff=lfs merge=lfs -text
+*.adp filter=lfs diff=lfs merge=lfs -text
+*.mpeg filter=lfs diff=lfs merge=lfs -text
+*.npx filter=lfs diff=lfs merge=lfs -text
+*.pdb filter=lfs diff=lfs merge=lfs -text
+*.PNG filter=lfs diff=lfs merge=lfs -text
+*.xwd filter=lfs diff=lfs merge=lfs -text
+*.egg filter=lfs diff=lfs merge=lfs -text
+*.ppsx filter=lfs diff=lfs merge=lfs -text
+*.mp4a filter=lfs diff=lfs merge=lfs -text
+*.pages filter=lfs diff=lfs merge=lfs -text
+*.baml filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.class filter=lfs diff=lfs merge=lfs -text
+*.h264 filter=lfs diff=lfs merge=lfs -text
+*.lib filter=lfs diff=lfs merge=lfs -text
+*.mmr filter=lfs diff=lfs merge=lfs -text
+*.dot filter=lfs diff=lfs merge=lfs -text
+*.gif filter=lfs diff=lfs merge=lfs -text
+*.JPG filter=lfs diff=lfs merge=lfs -text
+*.m4a filter=lfs diff=lfs merge=lfs -text
+*.so filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.thmx filter=lfs diff=lfs merge=lfs -text
+*.3ds filter=lfs diff=lfs merge=lfs -text
+*.bmp filter=lfs diff=lfs merge=lfs -text
+*.ogv filter=lfs diff=lfs merge=lfs -text
+*.xif filter=lfs diff=lfs merge=lfs -text
+*.aiff filter=lfs diff=lfs merge=lfs -text
+*.dts filter=lfs diff=lfs merge=lfs -text
+*.rip filter=lfs diff=lfs merge=lfs -text
+*.vob filter=lfs diff=lfs merge=lfs -text
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.fh filter=lfs diff=lfs merge=lfs -text
+*.flac filter=lfs diff=lfs merge=lfs -text
+*.g3 filter=lfs diff=lfs merge=lfs -text
+*.jpm filter=lfs diff=lfs merge=lfs -text
+*.ppsm filter=lfs diff=lfs merge=lfs -text
+*.potx filter=lfs diff=lfs merge=lfs -text
+*.zipx filter=lfs diff=lfs merge=lfs -text
+*.dsk filter=lfs diff=lfs merge=lfs -text
+*.ico filter=lfs diff=lfs merge=lfs -text
+*.ktx filter=lfs diff=lfs merge=lfs -text
+*.lz filter=lfs diff=lfs merge=lfs -text
+*.numbers filter=lfs diff=lfs merge=lfs -text
+*.3gp filter=lfs diff=lfs merge=lfs -text
+*.fst filter=lfs diff=lfs merge=lfs -text
+*.scpt filter=lfs diff=lfs merge=lfs -text
+*.epub filter=lfs diff=lfs merge=lfs -text
+*.rmvb filter=lfs diff=lfs merge=lfs -text
+*.webm filter=lfs diff=lfs merge=lfs -text
+*.docx filter=lfs diff=lfs merge=lfs -text
+*.pgm filter=lfs diff=lfs merge=lfs -text
+*.pya filter=lfs diff=lfs merge=lfs -text
+*.rtf filter=lfs diff=lfs merge=lfs -text
+*.smv filter=lfs diff=lfs merge=lfs -text
+*.tga filter=lfs diff=lfs merge=lfs -text
+*.cur filter=lfs diff=lfs merge=lfs -text
+*.dwg filter=lfs diff=lfs merge=lfs -text
+*.lvp filter=lfs diff=lfs merge=lfs -text
+*.pyo filter=lfs diff=lfs merge=lfs -text
+*.apk filter=lfs diff=lfs merge=lfs -text
+*.ar filter=lfs diff=lfs merge=lfs -text
+*.caf filter=lfs diff=lfs merge=lfs -text
+*.doc filter=lfs diff=lfs merge=lfs -text
+*.h263 filter=lfs diff=lfs merge=lfs -text
+*.xlsm filter=lfs diff=lfs merge=lfs -text
+*.mp3 filter=lfs diff=lfs merge=lfs -text
+*.mxu filter=lfs diff=lfs merge=lfs -text
+*.wax filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.mj2 filter=lfs diff=lfs merge=lfs -text
+*.otf filter=lfs diff=lfs merge=lfs -text
+*.udf filter=lfs diff=lfs merge=lfs -text
+*.aif filter=lfs diff=lfs merge=lfs -text
+*.lzma filter=lfs diff=lfs merge=lfs -text
+*.pyc filter=lfs diff=lfs merge=lfs -text
+*.weba filter=lfs diff=lfs merge=lfs -text
+*.webp filter=lfs diff=lfs merge=lfs -text
+*.cgm filter=lfs diff=lfs merge=lfs -text
+*.mkv filter=lfs diff=lfs merge=lfs -text
+*.ppa filter=lfs diff=lfs merge=lfs -text
+*.uvh filter=lfs diff=lfs merge=lfs -text
+*.xpi filter=lfs diff=lfs merge=lfs -text
+*.psd filter=lfs diff=lfs merge=lfs -text
+*.xlsb filter=lfs diff=lfs merge=lfs -text
+*.tbz filter=lfs diff=lfs merge=lfs -text
+*.wim filter=lfs diff=lfs merge=lfs -text
+*.ape filter=lfs diff=lfs merge=lfs -text
+*.avi filter=lfs diff=lfs merge=lfs -text
+*.dex filter=lfs diff=lfs merge=lfs -text
+*.dra filter=lfs diff=lfs merge=lfs -text
+*.dvb filter=lfs diff=lfs merge=lfs -text
+*.jpg filter=lfs diff=lfs merge=lfs -text
+*.xla filter=lfs diff=lfs merge=lfs -text
+*.fvt filter=lfs diff=lfs merge=lfs -text
+*.lzo filter=lfs diff=lfs merge=lfs -text
+*.pea filter=lfs diff=lfs merge=lfs -text
+*.ras filter=lfs diff=lfs merge=lfs -text
+*.tlz filter=lfs diff=lfs merge=lfs -text
+*.viv filter=lfs diff=lfs merge=lfs -text
+*.winmd filter=lfs diff=lfs merge=lfs -text
diff --git a/images/logo.png b/images/logo.png
new file mode 100644
index 0000000..70c7e75
--- /dev/null
+++ b/images/logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:722b25de1d0f7577e122a4d96c9d96f4019f7a34283a3612ae244a68eea7fc17
+size 47204
diff --git a/src/ReactiveMarbles.Command.Tests/FakeCommand.cs b/src/ReactiveMarbles.Command.Tests/FakeCommand.cs
new file mode 100644
index 0000000..feb5714
--- /dev/null
+++ b/src/ReactiveMarbles.Command.Tests/FakeCommand.cs
@@ -0,0 +1,63 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Windows.Input;
+
+namespace ReactiveMarbles.Command.Tests;
+
+///
+/// A fake command that can be executed as part of a test.
+///
+public class FakeCommand : ICommand
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public FakeCommand()
+ {
+ CanExecuteParameter = default;
+ ExecuteParameter = default;
+ }
+
+ ///
+ /// Occurs when changes occur that affect whether or not the command should execute.
+ ///
+ public event EventHandler? CanExecuteChanged;
+
+ ///
+ /// Gets the can execute parameter.
+ ///
+ public object? CanExecuteParameter { get; private set; }
+
+ ///
+ /// Gets the execute parameter.
+ ///
+ public object? ExecuteParameter { get; private set; }
+
+ ///
+ /// Defines the method that determines whether the command can execute in its current state.
+ ///
+ /// Data used by the command. If the command does not require data to be passed, this object can be set to .
+ ///
+ /// if this command can be executed; otherwise, .
+ ///
+ public bool CanExecute(object? parameter)
+ {
+ CanExecuteParameter = parameter;
+ return true;
+ }
+
+ ///
+ /// Defines the method to be called when the command is invoked.
+ ///
+ /// Data used by the command. If the command does not require data to be passed, this object can be set to .
+ public void Execute(object? parameter) => ExecuteParameter = parameter;
+
+ ///
+ /// Notifies the can execute changed.
+ ///
+ /// The instance containing the event data.
+ protected virtual void NotifyCanExecuteChanged(EventArgs e) => CanExecuteChanged?.Invoke(this, e);
+}
diff --git a/src/ReactiveMarbles.Command.Tests/ICommandHolder.cs b/src/ReactiveMarbles.Command.Tests/ICommandHolder.cs
new file mode 100644
index 0000000..835ba22
--- /dev/null
+++ b/src/ReactiveMarbles.Command.Tests/ICommandHolder.cs
@@ -0,0 +1,25 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Windows.Input;
+using ReactiveMarbles.Mvvm;
+
+namespace ReactiveMarbles.Command.Tests;
+
+///
+/// A ReactiveObject which hosts a command.
+///
+public class ICommandHolder : RxObject
+{
+ private ICommand? _theCommand;
+
+ ///
+ /// Gets or sets the command.
+ ///
+ public ICommand? TheCommand
+ {
+ get => _theCommand;
+ set => RaiseAndSetIfChanged(ref _theCommand, value);
+ }
+}
diff --git a/src/ReactiveMarbles.Command.Tests/ReactiveMarbles.Command.Tests.csproj b/src/ReactiveMarbles.Command.Tests/ReactiveMarbles.Command.Tests.csproj
new file mode 100644
index 0000000..b82f512
--- /dev/null
+++ b/src/ReactiveMarbles.Command.Tests/ReactiveMarbles.Command.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net6.0
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/src/ReactiveMarbles.Command.Tests/RxCommandHolder.cs b/src/ReactiveMarbles.Command.Tests/RxCommandHolder.cs
new file mode 100644
index 0000000..955cbc4
--- /dev/null
+++ b/src/ReactiveMarbles.Command.Tests/RxCommandHolder.cs
@@ -0,0 +1,26 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive;
+using ReactiveMarbles.Mvvm;
+
+namespace ReactiveMarbles.Command.Tests;
+
+///
+/// A ReactiveObject which hosts a ReactiveCommand.
+///
+///
+public class RxCommandHolder : RxObject
+{
+ private RxCommand? _theCommand;
+
+ ///
+ /// Gets or sets the command.
+ ///
+ public RxCommand? TheCommand
+ {
+ get => _theCommand;
+ set => RaiseAndSetIfChanged(ref _theCommand, value);
+ }
+}
diff --git a/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs b/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
new file mode 100644
index 0000000..9abf3a5
--- /dev/null
+++ b/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
@@ -0,0 +1,1315 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using System.Reactive.Concurrency;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using DynamicData;
+using DynamicData.Binding;
+using FluentAssertions;
+using Microsoft.Reactive.Testing;
+using ReactiveMarbles.Locator;
+using ReactiveMarbles.Mvvm;
+using ReactiveUI.Testing;
+using Xunit;
+
+namespace ReactiveMarbles.Command.Tests;
+
+///
+/// Tests for the RxCommand class.
+///
+public class RxCommandTests
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RxCommandTests() => ServiceLocator.Current().AddCoreRegistrations(() =>
+ CoreRegistrationBuilder
+ .Create()
+ .WithMainThreadScheduler(new TestScheduler())
+ .WithTaskPoolScheduler(new TestScheduler())
+ .WithExceptionHandler(new DebugExceptionHandler())
+ .Build());
+
+ ///
+ /// A test that determines whether this instance [can execute changed is available via ICommand].
+ ///
+ [Fact]
+ public void CanExecuteChangedIsAvailableViaICommand()
+ {
+ var canExecuteSubject = new Subject();
+ ICommand fixture = RxCommand.Create(() => Observable.Return(Unit.Default), canExecuteSubject, ImmediateScheduler.Instance);
+ var canExecuteChanged = new List();
+ fixture.CanExecuteChanged += (s, e) => canExecuteChanged.Add(fixture.CanExecute(null));
+
+ canExecuteSubject.OnNext(true);
+ canExecuteSubject.OnNext(false);
+
+ canExecuteChanged
+ .Should()
+ .HaveCount(2)
+ .And
+ .Subject
+ .Should()
+ .SatisfyRespectively(first => first.Should().BeTrue(), second => second.Should().BeFalse());
+ }
+
+ ///
+ /// A test that determines whether this instance [can execute is available via ICommand].
+ ///
+ [Fact]
+ public void CanExecuteIsAvailableViaICommand()
+ {
+ var canExecuteSubject = new Subject();
+ ICommand fixture = RxCommand.Create(() => Observable.Return(Unit.Default), canExecuteSubject, ImmediateScheduler.Instance);
+
+ Assert.False(fixture.CanExecute(null));
+
+ canExecuteSubject.OnNext(true);
+ fixture.CanExecute(null).Should().BeTrue();
+
+ canExecuteSubject.OnNext(false);
+
+ fixture.CanExecute(null).Should().BeFalse();
+ }
+
+ ///
+ /// Test that determines whether this instance [can execute is behavioral].
+ ///
+ [Fact]
+ public void CanExecuteIsBehavioral()
+ {
+ var fixture = RxCommand.Create(() => Observable.Return(Unit.Default), outputScheduler: ImmediateScheduler.Instance);
+ fixture.CanCommandExecute.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var canExecute).Subscribe();
+
+ canExecute.Should().ContainSingle(x => x);
+ }
+
+ ///
+ /// Test that determines whether this instance [can execute is false if already executing].
+ ///
+ [Fact]
+ public void CanExecuteIsFalseIfAlreadyExecuting() =>
+ new TestScheduler().With(
+ scheduler =>
+ {
+ var execute = Observable.Return(Unit.Default).Delay(TimeSpan.FromSeconds(1), scheduler);
+ var fixture = RxCommand.Create(() => execute, outputScheduler: scheduler);
+ fixture.CanCommandExecute.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var canExecute)
+ .Subscribe();
+
+ fixture.Execute().Subscribe();
+ scheduler.AdvanceByMs(100);
+
+ canExecute.Should().HaveCount(2);
+ canExecute[1].Should().BeFalse();
+
+ scheduler.AdvanceByMs(901);
+
+ canExecute.Should().HaveCount(3);
+ canExecute[2].Should().BeTrue();
+ });
+
+ ///
+ /// Test that determines whether this instance [can execute is false if caller dictates as such].
+ ///
+ [Fact]
+ public void CanExecuteIsFalseIfCallerDictatesAsSuch()
+ {
+ var canExecuteSubject = new Subject();
+ var fixture = RxCommand.Create(() => Observable.Return(Unit.Default), canExecuteSubject, ImmediateScheduler.Instance);
+ fixture.CanCommandExecute.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var canExecute).Subscribe();
+
+ canExecuteSubject.OnNext(true);
+ canExecuteSubject.OnNext(false);
+
+ canExecute
+ .Should()
+ .HaveCount(3)
+ .And
+ .Subject
+ .Should()
+ .SatisfyRespectively(
+ first => first.Should()
+ .BeFalse(),
+ second => second.Should()
+ .BeTrue(),
+ third => third.Should()
+ .BeFalse());
+ }
+
+ ///
+ /// Test that determines whether this instance [can execute is unsubscribed after command disposal].
+ ///
+ [Fact]
+ public void CanExecuteIsUnsubscribedAfterCommandDisposal()
+ {
+ var canExecuteSubject = new Subject();
+ var fixture = RxCommand.Create(() => Observable.Return(Unit.Default), canExecuteSubject, ImmediateScheduler.Instance);
+
+ Assert.True(canExecuteSubject.HasObservers);
+
+ fixture.Dispose();
+
+ canExecuteSubject.HasObservers.Should().BeFalse();
+ }
+
+ ///
+ /// Test that determines whether this instance [can execute only ticks distinct values].
+ ///
+ [Fact]
+ public void CanExecuteOnlyTicksDistinctValues()
+ {
+ var canExecuteSubject = new Subject();
+ var fixture = RxCommand.Create(() => Observable.Return(Unit.Default), canExecuteSubject, ImmediateScheduler.Instance);
+ fixture.CanCommandExecute.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var canExecute).Subscribe();
+
+ canExecuteSubject.OnNext(false);
+ canExecuteSubject.OnNext(false);
+ canExecuteSubject.OnNext(false);
+ canExecuteSubject.OnNext(false);
+ canExecuteSubject.OnNext(true);
+ canExecuteSubject.OnNext(true);
+
+ canExecute
+ .Should()
+ .HaveCount(2)
+ .And.Subject.Should()
+ .SatisfyRespectively(
+ first => first.Should()
+ .BeFalse(),
+ second => second.Should()
+ .BeTrue());
+ }
+
+ ///
+ /// Test that determines whether this instance [can execute ticks failures through thrown exceptions].
+ ///
+ [Fact]
+ public void CanExecuteTicksFailuresThroughThrownExceptions()
+ {
+ var canExecuteSubject = new Subject();
+ var fixture = RxCommand.Create(() => Observable.Return(Unit.Default), canExecuteSubject, ImmediateScheduler.Instance);
+ fixture.ThrownExceptions.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var thrownExceptions)
+ .Subscribe();
+
+ canExecuteSubject.OnError(new InvalidOperationException("oops"));
+
+ thrownExceptions.Should().ContainSingle(x => x.Message == "oops");
+ }
+
+ ///
+ /// Creates the task facilitates TPL integration.
+ ///
+ [Fact]
+ public void CreateTaskFacilitatesTPLIntegration()
+ {
+ var fixture = RxCommand.Create(_ => Task.FromResult(13), outputScheduler: ImmediateScheduler.Instance);
+ fixture.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var results).Subscribe();
+
+ fixture.Execute().Subscribe();
+
+ results.Should().ContainSingle(x => x == 13);
+ }
+
+ ///
+ /// Creates the task facilitates TPL integration with parameter.
+ ///
+ [Fact]
+ public void CreateTaskFacilitatesTPLIntegrationWithParameter()
+ {
+ var fixture = RxCommand.Create(param => Task.FromResult(param + 1), outputScheduler: ImmediateScheduler.Instance);
+ fixture.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var results).Subscribe();
+
+ fixture.Execute(3).Subscribe();
+ fixture.Execute(41).Subscribe();
+
+ results
+ .Should()
+ .HaveCount(2)
+ .And
+ .Subject
+ .Should()
+ .SatisfyRespectively(
+ first => first.Should()
+ .Be(4),
+ second => second.Should()
+ .Be(42));
+ }
+
+ ///
+ /// Creates the throws if execution parameter is null.
+ ///
+ [Fact]
+ public void CreateThrowsIfExecutionParameterIsNull()
+ {
+#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
+#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type.
+ Assert.Throws(() => RxCommand.Create((Func)null));
+ Assert.Throws(() => RxCommand.Create((Action)null));
+ Assert.Throws(() => RxCommand.Create((Func)null));
+ Assert.Throws(() => RxCommand.Create((Func>)null));
+ Assert.Throws(() => RxCommand.Create((Func>)null));
+ Assert.Throws(() => RxCommand.Create((Func>)null));
+ Assert.Throws(() => RxCommand.Create((Func>)null));
+ Assert.Throws(() => RxCommand.Create((Func>)null));
+ Assert.Throws(() => RxCommand.Create((Func>)null));
+#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type.
+#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.
+ }
+
+ ///
+ /// Exceptions the are delivered on output scheduler.
+ ///
+ [Fact]
+ public void ExceptionsAreDeliveredOnOutputScheduler() =>
+ new TestScheduler().With(
+ scheduler =>
+ {
+ var fixture = RxCommand.Create(() => Observable.Throw(new InvalidOperationException()), outputScheduler: scheduler);
+ Exception? exception = null;
+ fixture.ThrownExceptions.Subscribe(ex => exception = ex);
+ fixture.Execute().Subscribe(_ => { }, _ => { });
+
+ Assert.Null(exception);
+ scheduler.Start();
+ exception.Should().BeOfType();
+ });
+
+ ///
+ /// Executes the can be cancelled.
+ ///
+ [Fact]
+ public void ExecuteCanBeCancelled() =>
+ new TestScheduler().With(
+ scheduler =>
+ {
+ var execute = Observable.Return(Unit.Default).Delay(TimeSpan.FromSeconds(1), scheduler);
+ var fixture = RxCommand.Create(() => execute, outputScheduler: scheduler);
+ fixture.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var executed).Subscribe();
+
+ var sub1 = fixture.Execute().Subscribe();
+ var sub2 = fixture.Execute().Subscribe();
+ scheduler.AdvanceByMs(999);
+
+ Assert.True(fixture.IsExecuting.FirstAsync().Wait());
+ Assert.Empty(executed);
+ sub1.Dispose();
+
+ scheduler.AdvanceByMs(2);
+ Assert.Equal(1, executed.Count);
+ Assert.False(fixture.IsExecuting.FirstAsync().Wait());
+ });
+
+ ///
+ /// Executes the can tick through multiple results.
+ ///
+ [Fact]
+ public void ExecuteCanTickThroughMultipleResults()
+ {
+ var fixture = RxCommand.Create(
+ () => new[] { 1, 2, 3 }.ToObservable(),
+ outputScheduler: ImmediateScheduler.Instance);
+ fixture.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var results).Subscribe();
+
+ fixture.Execute().Subscribe();
+
+ results
+ .Should()
+ .HaveCount(3)
+ .And.Subject.Should()
+ .SatisfyRespectively(
+ first => first.Should()
+ .Be(1),
+ second => second.Should()
+ .Be(2),
+ third => third.Should()
+ .Be(3));
+ }
+
+ ///
+ /// Executes the facilitates any number of in flight executions.
+ ///
+ [Fact]
+ public void ExecuteFacilitatesAnyNumberOfInFlightExecutions() =>
+ new TestScheduler().With(
+ scheduler =>
+ {
+ var execute = Observable.Return(Unit.Default).Delay(TimeSpan.FromMilliseconds(500), scheduler);
+ var fixture = RxCommand.Create(() => execute, outputScheduler: scheduler);
+ fixture.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var executed).Subscribe();
+
+ var sub1 = fixture.Execute().Subscribe();
+ var sub2 = fixture.Execute().Subscribe();
+ scheduler.AdvanceByMs(100);
+
+ var sub3 = fixture.Execute().Subscribe();
+ scheduler.AdvanceByMs(200);
+ var sub4 = fixture.Execute().Subscribe();
+ scheduler.AdvanceByMs(100);
+
+ Assert.True(fixture.IsExecuting.FirstAsync().Wait());
+ Assert.Empty(executed);
+
+ scheduler.AdvanceByMs(101);
+ Assert.Equal(2, executed.Count);
+ Assert.True(fixture.IsExecuting.FirstAsync().Wait());
+
+ scheduler.AdvanceByMs(200);
+ Assert.Equal(3, executed.Count);
+ Assert.True(fixture.IsExecuting.FirstAsync().Wait());
+
+ scheduler.AdvanceByMs(100);
+ Assert.Equal(4, executed.Count);
+ Assert.False(fixture.IsExecuting.FirstAsync().Wait());
+ });
+
+ ///
+ /// Executes the is available via ICommand.
+ ///
+ [Fact]
+ public void ExecuteIsAvailableViaICommand()
+ {
+ var executed = false;
+ ICommand fixture = RxCommand.Create(
+ () =>
+ {
+ executed = true;
+ return Observable.Return(Unit.Default);
+ },
+ outputScheduler: ImmediateScheduler.Instance);
+
+ fixture.Execute(null);
+ executed.Should().BeTrue();
+ }
+
+ ///
+ /// Executes the passes through parameter.
+ ///
+ [Fact]
+ public void ExecutePassesThroughParameter()
+ {
+ var parameters = new List();
+ var fixture = RxCommand.Create(
+ param =>
+ {
+ parameters.Add(param);
+ return Observable.Return(Unit.Default);
+ },
+ outputScheduler: ImmediateScheduler.Instance);
+
+ fixture.Execute(1).Subscribe();
+ fixture.Execute(42).Subscribe();
+ fixture.Execute(348).Subscribe();
+
+ parameters
+ .Should()
+ .HaveCount(3)
+ .And
+ .Subject
+ .Should()
+ .SatisfyRespectively(
+ first => first.Should().Be(1),
+ second => second.Should().Be(42),
+ third => third.Should().Be(348));
+ }
+
+ ///
+ /// Executes the reenables execution even after failure.
+ ///
+ [Fact]
+ public void ExecuteReenablesExecutionEvenAfterFailure()
+ {
+ var fixture = RxCommand.Create(() => Observable.Throw(new InvalidOperationException("oops")), outputScheduler: ImmediateScheduler.Instance);
+ fixture.CanCommandExecute.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var canExecute).Subscribe();
+ fixture.ThrownExceptions.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var thrownExceptions)
+ .Subscribe();
+
+ fixture.Execute().Subscribe(_ => { }, _ => { });
+
+ Assert.Equal(1, thrownExceptions.Count);
+ Assert.Equal("oops", thrownExceptions[0].Message);
+
+ canExecute
+ .Should()
+ .HaveCount(3)
+ .And
+ .Subject
+ .Should()
+ .SatisfyRespectively(
+ first => first.Should().BeTrue(),
+ second => second.Should().BeFalse(),
+ third => third.Should().BeTrue());
+ }
+
+ ///
+ /// Executes the result is delivered on specified scheduler.
+ ///
+ [Fact]
+ public void ExecuteResultIsDeliveredOnSpecifiedScheduler() =>
+ new TestScheduler().With(
+ scheduler =>
+ {
+ var execute = Observable.Return(Unit.Default);
+ var fixture = RxCommand.Create(() => execute, outputScheduler: scheduler);
+ var executed = false;
+
+ fixture.Execute().ObserveOn(scheduler).Subscribe(_ => executed = true);
+
+ executed.Should().BeFalse();
+ scheduler.AdvanceByMs(1);
+ executed.Should().BeTrue();
+ });
+
+ ///
+ /// Executes the ticks any exception.
+ ///
+ [Fact]
+ public void ExecuteTicksAnyException()
+ {
+ var fixture = RxCommand.Create(() => Observable.Throw(new InvalidOperationException()), outputScheduler: ImmediateScheduler.Instance);
+ fixture.ThrownExceptions.Subscribe();
+ Exception? exception = null;
+ fixture.Execute().Subscribe(_ => { }, ex => exception = ex, () => { });
+
+ exception.Should().BeOfType();
+ }
+
+ ///
+ /// Executes the ticks any lambda exception.
+ ///
+ [Fact]
+ public void ExecuteTicksAnyLambdaException()
+ {
+ Unit Execute() => throw new InvalidOperationException();
+ var fixture = RxCommand.Create(Execute, outputScheduler: ImmediateScheduler.Instance);
+ fixture.ThrownExceptions.Subscribe();
+ Exception? exception = null;
+ fixture.Execute().Subscribe(_ => { }, ex => exception = ex, () => { });
+
+ exception.Should().BeOfType();
+ }
+
+ ///
+ /// Executes the ticks errors through thrown exceptions.
+ ///
+ [Fact]
+ public void ExecuteTicksErrorsThroughThrownExceptions()
+ {
+ var fixture = RxCommand.Create(() => Observable.Throw(new InvalidOperationException("oops")), outputScheduler: ImmediateScheduler.Instance);
+ fixture.ThrownExceptions.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var thrownExceptions)
+ .Subscribe();
+
+ fixture.Execute().Subscribe(_ => { }, _ => { });
+
+ thrownExceptions.Should().ContainSingle(x => x.Message == "oops");
+ }
+
+ ///
+ /// Executes the ticks lambda errors through thrown exceptions.
+ ///
+ [Fact]
+ public void ExecuteTicksLambdaErrorsThroughThrownExceptions()
+ {
+ Unit Execute() => throw new InvalidOperationException("oops");
+ var fixture = RxCommand.Create(Execute, outputScheduler: ImmediateScheduler.Instance);
+ fixture.ThrownExceptions.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var thrownExceptions)
+ .Subscribe();
+
+ fixture.Execute().Subscribe(_ => { }, _ => { });
+
+ Assert.Equal(1, thrownExceptions.Count);
+ Assert.Equal("oops", thrownExceptions[0].Message);
+ Assert.True(fixture.CanCommandExecute.FirstAsync().Wait());
+ }
+
+ ///
+ /// Executes the ticks through the result.
+ ///
+ [Fact]
+ public void ExecuteTicksThroughTheResult()
+ {
+ var num = 0;
+ var fixture = RxCommand.Create(() => Observable.Return(num), outputScheduler: ImmediateScheduler.Instance);
+ fixture.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var results).Subscribe();
+
+ num = 1;
+ fixture.Execute().Subscribe();
+ num = 10;
+ fixture.Execute().Subscribe();
+ num = 30;
+ fixture.Execute().Subscribe();
+
+ results
+ .Should()
+ .HaveCount(3)
+ .And
+ .Subject
+ .Should()
+ .SatisfyRespectively(
+ first => first.Should().Be(1),
+ second => second.Should().Be(10),
+ third => third.Should().Be(30));
+ }
+
+ ///
+ /// Executes via ICommand throws if parameter type is incorrect.
+ ///
+ [Fact]
+ public void ExecuteViaICommandThrowsIfParameterTypeIsIncorrect()
+ {
+ ICommand fixture = RxCommand.Create(_ => { }, outputScheduler: ImmediateScheduler.Instance);
+ var ex = Assert.Throws(() => fixture.Execute("foo"));
+ Assert.Equal("Command requires parameters of type System.Int32, but received parameter of type System.String.", ex.Message);
+
+ fixture = RxCommand.Create(_ => { });
+ ex = Assert.Throws(() => fixture.Execute(13));
+
+ ex.Message.Should()
+ .Be("Command requires parameters of type System.String, but received parameter of type System.Int32.");
+ }
+
+ ///
+ /// Executes via ICommand works with nullable types.
+ ///
+ [Fact]
+ public void ExecuteViaICommandWorksWithNullableTypes()
+ {
+ int? value = null;
+ ICommand fixture =
+ RxCommand.Create(param => value = param, outputScheduler: ImmediateScheduler.Instance);
+
+ fixture.Execute(42);
+ value.Should().Be(42);
+
+ fixture.Execute(null);
+ value.Should().BeNull();
+ }
+
+ ///
+ /// Test that invokes the command against ICommand in target invokes the command.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInTargetInvokesTheCommand()
+ {
+ var executionCount = 0;
+ var fixture = new ICommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ fixture.TheCommand = RxCommand.Create(() => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(1);
+ Assert.Equal(1, executionCount);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against ICommand in target passes the specified value to can execute and execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInTargetPassesTheSpecifiedValueToCanExecuteAndExecute()
+ {
+ var fixture = new ICommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ var command = new FakeCommand();
+ fixture.TheCommand = command;
+
+ source.OnNext(42);
+ command.ExecuteParameter.Should().Be(42);
+ command.CanExecuteParameter.Should().Be(42);
+ }
+
+ ///
+ /// Test that invokes the command against ICommand in target passes the specified value to can execute and execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInNullableTargetPassesTheSpecifiedValueToCanExecuteAndExecute()
+ {
+ var fixture = new ICommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand);
+ var command = new FakeCommand();
+ fixture.TheCommand = command;
+
+ source.OnNext(42);
+ command.ExecuteParameter.Should().Be(42);
+ command.CanExecuteParameter.Should().Be(42);
+ }
+
+ ///
+ /// Test that invokes the command against i command in target respects can execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInTargetRespectsCanExecute()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ var fixture = new ICommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ fixture.TheCommand = RxCommand.Create(() => executed = true, canExecute, ImmediateScheduler.Instance);
+
+ source.OnNext(Unit.Default);
+ executed.Should().BeFalse();
+
+ canExecute.OnNext(true);
+ source.OnNext(Unit.Default);
+ executed.Should().BeTrue();
+ }
+
+ ///
+ /// Test that invokes the command against i command in target respects can execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInNullableTargetRespectsCanExecute()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ var fixture = new ICommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand);
+ fixture.TheCommand = RxCommand.Create(() => executed = true, canExecute, ImmediateScheduler.Instance);
+
+ source.OnNext(Unit.Default);
+ executed.Should().BeFalse();
+
+ canExecute.OnNext(true);
+ source.OnNext(Unit.Default);
+ executed.Should().BeTrue();
+ }
+
+ ///
+ /// Test that invokes the command against ICommand in target respects can execute window.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInTargetRespectsCanExecuteWindow()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ var fixture = new ICommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ fixture.TheCommand = RxCommand.Create(() => executed = true, canExecute, ImmediateScheduler.Instance);
+
+ source.OnNext(Unit.Default);
+ executed.Should().BeFalse();
+
+ // The execution window re-opens, but the above execution request should not be instigated because
+ // it occurred when the window was closed. Execution requests do not queue up when the window is closed.
+ canExecute.OnNext(true);
+ executed.Should().BeFalse();
+ }
+
+ ///
+ /// Test that invokes the command against ICommand in target swallows exceptions.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInTargetSwallowsExceptions()
+ {
+ var count = 0;
+ var fixture = new ICommandHolder();
+ var command = RxCommand.Create(
+ () =>
+ {
+ ++count;
+ throw new InvalidOperationException();
+ },
+ outputScheduler: ImmediateScheduler.Instance);
+ command.ThrownExceptions.Subscribe();
+ fixture.TheCommand = command;
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+
+ source.OnNext(Unit.Default);
+ source.OnNext(Unit.Default);
+
+ count.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against ICommand invokes the command.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandInvokesTheCommand()
+ {
+ var executionCount = 0;
+ ICommand fixture = RxCommand.Create(() => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(1);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against ICommand invokes the command.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstNullableICommandInvokesTheCommand()
+ {
+ var executionCount = 0;
+ ICommand fixture = RxCommand.Create(() => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(1);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against ICommand passes the specified value to can execute and execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandPassesTheSpecifiedValueToCanExecuteAndExecute()
+ {
+ var fixture = new FakeCommand();
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(42);
+
+ fixture.ExecuteParameter.Should().Be(42);
+ fixture.CanExecuteParameter.Should().Be(42);
+ }
+
+ ///
+ /// Test that invokes the command against ICommand respects can execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandRespectsCanExecute()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ ICommand fixture = RxCommand.Create(() => executed = true, canExecute, ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ executed.Should().BeFalse();
+
+ canExecute.OnNext(true);
+ source.OnNext(Unit.Default);
+ executed.Should().BeTrue();
+ }
+
+ ///
+ /// Test that invokes the command against ICommand respects can execute window.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandRespectsCanExecuteWindow()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ ICommand fixture = RxCommand.Create(() => executed = true, canExecute, ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ executed.Should().BeFalse();
+
+ // The execution window re-opens, but the above execution request should not be instigated because
+ // it occurred when the window was closed. Execution requests do not queue up when the window is closed.
+ canExecute.OnNext(true);
+ executed.Should().BeFalse();
+ }
+
+ ///
+ /// Test that invokes the command against ICommand swallows exceptions.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstICommandSwallowsExceptions()
+ {
+ var count = 0;
+ var fixture = RxCommand.Create(
+ () =>
+ {
+ ++count;
+ throw new InvalidOperationException();
+ },
+ outputScheduler: ImmediateScheduler.Instance);
+ fixture.ThrownExceptions.Subscribe();
+ var source = new Subject();
+ source.InvokeCommand((ICommand)fixture);
+
+ source.OnNext(Unit.Default);
+ source.OnNext(Unit.Default);
+
+ count.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against reactive command in target invokes the command.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandInTargetInvokesTheCommand()
+ {
+ var executionCount = 0;
+ var fixture = new RxCommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ fixture.TheCommand = RxCommand.Create(_ => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+
+ source.OnNext(0);
+ executionCount.Should().Be(1);
+
+ source.OnNext(0);
+ executionCount.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against reactive command in target passes the specified value to execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandInTargetPassesTheSpecifiedValueToExecute()
+ {
+ var executeReceived = 0;
+ var fixture = new RxCommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ fixture.TheCommand =
+ RxCommand.Create(x => executeReceived = x, outputScheduler: ImmediateScheduler.Instance);
+
+ source.OnNext(42);
+ executeReceived.Should().Be(42);
+ }
+
+ ///
+ /// Test that invokes the command against reactive command in target respects can execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandInTargetRespectsCanExecute()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ var fixture = new RxCommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ fixture.TheCommand = RxCommand.Create(_ => executed = true, canExecute, ImmediateScheduler.Instance);
+
+ source.OnNext(0);
+ executed.Should().BeFalse();
+
+ canExecute.OnNext(true);
+ source.OnNext(0);
+ executed.Should().BeTrue();
+ }
+
+ ///
+ /// Test that invokes the command against reactive command in target respects can execute window.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandInTargetRespectsCanExecuteWindow()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ var fixture = new RxCommandHolder();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+ fixture.TheCommand = RxCommand.Create(_ => executed = true, canExecute, ImmediateScheduler.Instance);
+
+ source.OnNext(0);
+ executed.Should().BeFalse();
+
+ // The execution window re-opens, but the above execution request should not be instigated because
+ // it occurred when the window was closed. Execution requests do not queue up when the window is closed.
+ canExecute.OnNext(true);
+ executed.Should().BeFalse();
+ }
+
+ ///
+ /// Test that invokes the command against reactive command in target swallows exceptions.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandInTargetSwallowsExceptions()
+ {
+ var count = 0;
+ var fixture = new RxCommandHolder()
+ {
+ TheCommand = RxCommand.Create(
+ _ =>
+ {
+ ++count;
+ throw new InvalidOperationException();
+ },
+ outputScheduler: ImmediateScheduler.Instance)
+ };
+ fixture.TheCommand.ThrownExceptions.Subscribe();
+ var source = new Subject();
+ source.InvokeCommand(fixture, x => x.TheCommand!);
+
+ source.OnNext(0);
+ source.OnNext(0);
+
+ count.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against reactive command invokes the command.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandInvokesTheCommand()
+ {
+ var executionCount = 0;
+ var fixture = RxCommand.Create(() => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(1);
+
+ source.OnNext(Unit.Default);
+ executionCount.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command against reactive command passes the specified value to execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandPassesTheSpecifiedValueToExecute()
+ {
+ var executeReceived = 0;
+ var fixture = RxCommand.Create(x => executeReceived = x, outputScheduler: ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(42);
+ executeReceived.Should().Be(42);
+ }
+
+ ///
+ /// Test that invokes the command against reactive command respects can execute.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandRespectsCanExecute()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ var fixture = RxCommand.Create(() => executed = true, canExecute, ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ executed.Should().BeFalse();
+
+ canExecute.OnNext(true);
+ source.OnNext(Unit.Default);
+ executed.Should().BeTrue();
+ }
+
+ ///
+ /// Test that invokes the command against reactive command respects can execute window.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandRespectsCanExecuteWindow()
+ {
+ var executed = false;
+ var canExecute = new BehaviorSubject(false);
+ var fixture = RxCommand.Create(() => executed = true, canExecute, outputScheduler: ImmediateScheduler.Instance);
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ executed.Should().BeFalse();
+
+ // The execution window re-opens, but the above execution request should not be instigated because
+ // it occurred when the window was closed. Execution requests do not queue up when the window is closed.
+ canExecute.OnNext(true);
+ executed.Should().BeFalse();
+ }
+
+ ///
+ /// Test that invokes the command against reactive command swallows exceptions.
+ ///
+ [Fact]
+ public void InvokeCommandAgainstRxCommandSwallowsExceptions()
+ {
+ var count = 0;
+ var fixture = RxCommand.Create(
+ () =>
+ {
+ ++count;
+ throw new InvalidOperationException();
+ },
+ outputScheduler: ImmediateScheduler.Instance);
+ fixture.ThrownExceptions.Subscribe();
+ var source = new Subject();
+ source.InvokeCommand(fixture);
+
+ source.OnNext(Unit.Default);
+ source.OnNext(Unit.Default);
+
+ count.Should().Be(2);
+ }
+
+ ///
+ /// Test that invokes the command works even if the source is cold.
+ ///
+ [Fact]
+ public void InvokeCommandWorksEvenIfTheSourceIsCold()
+ {
+ var executionCount = 0;
+ var fixture = RxCommand.Create(() => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+ var source = Observable.Return(Unit.Default);
+ source.InvokeCommand(fixture);
+
+ executionCount.Should().Be(1);
+ }
+
+ ///
+ /// Test that determines whether [is executing is behavioral].
+ ///
+ [Fact]
+ public void IsExecutingIsBehavioral()
+ {
+ var fixture = RxCommand.Create(
+ () => Observable.Return(Unit.Default),
+ outputScheduler: ImmediateScheduler.Instance);
+ fixture.IsExecuting.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var isExecuting).Subscribe();
+
+ isExecuting.Should().ContainSingle(x => !x);
+ }
+
+ ///
+ /// Test that determines whether [is executing remains true as long as execution pipeline has not completed].
+ ///
+ [Fact]
+ public void IsExecutingRemainsTrueAsLongAsExecutionPipelineHasNotCompleted()
+ {
+ var execute = new Subject();
+ var fixture = RxCommand.Create(() => execute, outputScheduler: ImmediateScheduler.Instance);
+
+ fixture.Execute().Subscribe();
+
+ Assert.True(fixture.IsExecuting.FirstAsync().Wait());
+
+ execute.OnNext(Unit.Default);
+ Assert.True(fixture.IsExecuting.FirstAsync().Wait());
+
+ execute.OnNext(Unit.Default);
+ Assert.True(fixture.IsExecuting.FirstAsync().Wait());
+
+ execute.OnCompleted();
+ Assert.False(fixture.IsExecuting.FirstAsync().Wait());
+ }
+
+ ///
+ /// Test that determines whether [is executing ticks as executions progress].
+ ///
+ [Fact]
+ public void IsExecutingTicksAsExecutionsProgress() =>
+ new TestScheduler().With(
+ scheduler =>
+ {
+ var execute = Observable.Return(Unit.Default).Delay(TimeSpan.FromSeconds(1), scheduler);
+ var fixture = RxCommand.Create(() => execute, outputScheduler: scheduler);
+ fixture.IsExecuting.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var isExecuting)
+ .Subscribe();
+
+ fixture.Execute().Subscribe();
+ scheduler.AdvanceByMs(100);
+
+ isExecuting.Should().HaveCount(2);
+ isExecuting.Should().SatisfyRespectively(
+ first => first.Should().BeFalse(),
+ second => second.Should().BeTrue());
+
+ scheduler.AdvanceByMs(901);
+
+ isExecuting.Should().HaveCount(3);
+ Assert.False(isExecuting[2]);
+ });
+
+ ///
+ /// Results the is ticked through specified scheduler.
+ ///
+ [Fact]
+ public void ResultIsTickedThroughSpecifiedScheduler() =>
+ new TestScheduler().With(
+ scheduler =>
+ {
+ var fixture = RxCommand.Create(() => Observable.Return(Unit.Default), outputScheduler: scheduler);
+ fixture.ToObservableChangeSet(ImmediateScheduler.Instance).Bind(out var results).Subscribe();
+
+ fixture.Execute().Subscribe();
+ results.Should().BeEmpty();
+
+ scheduler.AdvanceByMs(1);
+ results.Should().HaveCount(1);
+ });
+
+ ///
+ /// Synchronizes the command execute lazily.
+ ///
+ [Fact]
+ public void SynchronousCommandExecuteLazily()
+ {
+ var executionCount = 0;
+ var fixture1 = RxCommand.Create(() => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+ var fixture2 = RxCommand.Create(_ => ++executionCount, outputScheduler: ImmediateScheduler.Instance);
+ var fixture3 = RxCommand.Create(
+ () =>
+ {
+ ++executionCount;
+ return 42;
+ },
+ outputScheduler: ImmediateScheduler.Instance);
+ var fixture4 = RxCommand.Create(
+ _ =>
+ {
+ ++executionCount;
+ return 42;
+ },
+ outputScheduler: ImmediateScheduler.Instance);
+ var execute1 = fixture1.Execute();
+ var execute2 = fixture2.Execute();
+ var execute3 = fixture3.Execute();
+ var execute4 = fixture4.Execute();
+
+ executionCount.Should().Be(0);
+
+ execute1.Subscribe();
+ executionCount.Should().Be(1);
+
+ execute2.Subscribe();
+ executionCount.Should().Be(2);
+
+ execute3.Subscribe();
+ executionCount.Should().Be(3);
+
+ execute4.Subscribe();
+ executionCount.Should().Be(4);
+ }
+
+ ///
+ /// Synchronizes the commands fail correctly.
+ ///
+ [Fact]
+ public void SynchronousCommandsFailCorrectly()
+ {
+ var fixture1 = RxCommand.Create(
+ () => throw new InvalidOperationException(),
+ outputScheduler: ImmediateScheduler.Instance);
+ var fixture2 = RxCommand.Create(
+ _ => throw new InvalidOperationException(),
+ outputScheduler: ImmediateScheduler.Instance);
+ var fixture3 = RxCommand.Create(
+ () => throw new InvalidOperationException(),
+ outputScheduler: ImmediateScheduler.Instance);
+ Func execute = _ => throw new InvalidOperationException();
+ var fixture4 = RxCommand.Create(
+ execute,
+ outputScheduler: ImmediateScheduler.Instance);
+
+ var failureCount = 0;
+ Observable.Merge(
+ fixture1.ThrownExceptions,
+ fixture2.ThrownExceptions,
+ fixture3.ThrownExceptions,
+ fixture4.ThrownExceptions)
+ .Subscribe(_ => ++failureCount);
+
+ fixture1.Execute().Subscribe(_ => { }, _ => { });
+
+ failureCount.Should().Be(1);
+
+ fixture2.Execute().Subscribe(_ => { }, _ => { });
+ failureCount.Should().Be(2);
+
+ fixture3.Execute().Subscribe(_ => { }, _ => { });
+ failureCount.Should().Be(3);
+
+ fixture4.Execute().Subscribe(_ => { }, _ => { });
+ failureCount.Should().Be(4);
+ }
+
+ ///
+ /// Tests RxCommand from an invoke command.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task RxCommandCreateHandlesTaskExceptionAsync()
+ {
+ var subj = new Subject();
+ var isExecuting = false;
+ Exception? fail = null;
+
+ async Task Execute()
+ {
+ await subj.Take(1);
+ throw new Exception("break execution");
+ }
+
+ var fixture = RxCommand.Create(Execute);
+
+ var fixture = new RxCommand(
+ async _ =>
+ {
+ await subj.Take(1);
+ throw new Exception("break execution");
+ },
+ ObservableConstants.True,
+ ImmediateScheduler.Instance);
+ fixture.IsExecuting.Subscribe(x => isExecuting = x);
+ fixture.ThrownExceptions.Subscribe(ex => fail = ex);
+
+ isExecuting.Should().BeFalse();
+ fail.Should().BeNull();
+
+ fixture.Execute().Subscribe();
+ isExecuting.Should().BeTrue();
+ fail.Should().BeNull();
+
+ subj.OnNext(Unit.Default);
+
+ // Wait 10 ms to allow execution to complete
+ await Task.Delay(10).ConfigureAwait(false);
+
+ isExecuting.Should().BeFalse();
+ fail?.Message.Should().NotBeNull().And.Subject.Should().Be("break execution");
+ }
+
+ ///
+ /// Tests RxCommand from an invoke command.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task RxCommandExecutesFromInvokeCommand()
+ {
+ var semaphore = new SemaphoreSlim(0);
+ var command = RxCommand.Create(() => semaphore.Release());
+
+ Observable.Return(Unit.Default)
+ .InvokeCommand(command);
+
+ var result = 0;
+ var task = semaphore.WaitAsync();
+ if (await Task.WhenAny(Task.Delay(TimeSpan.FromMilliseconds(100)), task).ConfigureAwait(true) == task)
+ {
+ result = 1;
+ }
+ else
+ {
+ result = -1;
+ }
+
+ await Task.Delay(200).ConfigureAwait(false);
+ result.Should().Be(1);
+ }
+}
diff --git a/src/ReactiveMarbles.Command.sln b/src/ReactiveMarbles.Command.sln
new file mode 100644
index 0000000..d7092fd
--- /dev/null
+++ b/src/ReactiveMarbles.Command.sln
@@ -0,0 +1,22 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveMarbles.Command", "ReactiveMarbles.Command\ReactiveMarbles.Command.csproj", "{19AA5FBF-A095-4459-A523-03E5221AEF0F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReactiveMarbles.Command.Tests", "ReactiveMarbles.Command.Tests\ReactiveMarbles.Command.Tests.csproj", "{7D1573F5-9CEF-49EB-B653-87857230495A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {19AA5FBF-A095-4459-A523-03E5221AEF0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {19AA5FBF-A095-4459-A523-03E5221AEF0F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {19AA5FBF-A095-4459-A523-03E5221AEF0F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {19AA5FBF-A095-4459-A523-03E5221AEF0F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D1573F5-9CEF-49EB-B653-87857230495A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7D1573F5-9CEF-49EB-B653-87857230495A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D1573F5-9CEF-49EB-B653-87857230495A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7D1573F5-9CEF-49EB-B653-87857230495A}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+EndGlobal
diff --git a/src/ReactiveMarbles.Command/IRxCommand.cs b/src/ReactiveMarbles.Command/IRxCommand.cs
new file mode 100644
index 0000000..5396c76
--- /dev/null
+++ b/src/ReactiveMarbles.Command/IRxCommand.cs
@@ -0,0 +1,114 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Windows.Input;
+
+namespace ReactiveMarbles.Command;
+
+///
+/// Encapsulates a user action behind a reactive interface.
+/// This is for interop inside for the command binding.
+/// Not meant for external use due to the fact it doesn't implement ICommand
+/// to force the user to favor the Reactive style command execution.
+///
+public interface IRxCommand : ICommand, IDisposable
+{
+ ///
+ /// Gets an observable whose value indicates whether the command is currently executing.
+ ///
+ ///
+ /// This observable can be particularly useful for updating UI, such as showing an activity indicator whilst a command
+ /// is executing.
+ ///
+ IObservable IsExecuting { get; }
+
+ ///
+ /// Gets an observable whose value indicates whether the command can currently execute.
+ ///
+ ///
+ /// The value provided by this observable is governed both by any canExecute observable provided during
+ /// command creation, as well as the current execution status of the command. A command that is currently executing
+ /// will always yield false from this observable, even if the canExecute pipeline is currently true.
+ ///
+ IObservable CanCommandExecute { get; }
+
+ ///
+ /// Gets an observable that ticks any exceptions in command execution logic.
+ ///
+ ///
+ /// Any exceptions that are not observed via this observable will propagate out and cause the application to be torn
+ /// down. Therefore, you will always want to subscribe to this observable if you expect errors could occur (e.g. if
+ /// your command execution includes network activity).
+ ///
+ IObservable ThrownExceptions { get; }
+}
+
+///
+/// Encapsulates a user action behind a reactive interface.
+/// This is for interop inside for the command binding.
+/// Not meant for external use due to the fact it doesn't implement ICommand
+/// to force the user to favor the Reactive style command execution.
+///
+/// The parameter type passed to the command.
+/// The result type from the command.
+public interface IRxCommand : IRxCommand, IObservable
+{
+ ///
+ /// Gets an observable that, when subscribed, executes this command.
+ ///
+ ///
+ ///
+ /// Invoking this method will return a cold (lazy) observable that, when subscribed, will execute the logic
+ /// encapsulated by the command. It is worth restating that the returned observable is lazy. Nothing will
+ /// happen if you call Execute and neglect to subscribe (directly or indirectly) to the returned observable.
+ ///
+ ///
+ /// If no parameter value is provided, a default value of type will be passed into
+ /// the execution logic.
+ ///
+ ///
+ /// Any number of subscribers can subscribe to a given execution observable and the execution logic will only
+ /// run once. That is, the result is broadcast to those subscribers.
+ ///
+ ///
+ /// In those cases where execution fails, there will be no result value. Instead, the failure will tick through the
+ /// observable.
+ ///
+ ///
+ ///
+ /// An observable that will tick the single result value if and when it becomes available.
+ ///
+ IObservable Execute();
+
+ ///
+ /// Gets an observable that, when subscribed, executes this command.
+ ///
+ ///
+ ///
+ /// Invoking this method will return a cold (lazy) observable that, when subscribed, will execute the logic
+ /// encapsulated by the command. It is worth restating that the returned observable is lazy. Nothing will
+ /// happen if you call Execute and neglect to subscribe (directly or indirectly) to the returned observable.
+ ///
+ ///
+ /// If no parameter value is provided, a default value of type will be passed into
+ /// the execution logic.
+ ///
+ ///
+ /// Any number of subscribers can subscribe to a given execution observable and the execution logic will only
+ /// run once. That is, the result is broadcast to those subscribers.
+ ///
+ ///
+ /// In those cases where execution fails, there will be no result value. Instead, the failure will tick through the
+ /// observable.
+ ///
+ ///
+ ///
+ /// The parameter to pass into command execution.
+ ///
+ ///
+ /// An observable that will tick the single result value if and when it becomes available.
+ ///
+ IObservable Execute(TParam parameter);
+}
diff --git a/src/ReactiveMarbles.Command/ObservableConstants.cs b/src/ReactiveMarbles.Command/ObservableConstants.cs
new file mode 100644
index 0000000..5702305
--- /dev/null
+++ b/src/ReactiveMarbles.Command/ObservableConstants.cs
@@ -0,0 +1,19 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+
+namespace ReactiveMarbles.Command;
+
+internal static class ObservableConstants
+{
+ internal static readonly IObservable True = Observable.Return(true);
+ internal static readonly IObservable False = Observable.Return(false);
+}
+
+internal static class ObservableConstants
+{
+ internal static readonly IObservable Empty = Observable.Empty();
+}
diff --git a/src/ReactiveMarbles.Command/ReactiveMarbles.Command.csproj b/src/ReactiveMarbles.Command/ReactiveMarbles.Command.csproj
new file mode 100644
index 0000000..02300f9
--- /dev/null
+++ b/src/ReactiveMarbles.Command/ReactiveMarbles.Command.csproj
@@ -0,0 +1,18 @@
+
+
+
+ netstandard2.0;net6.0
+ enable
+ latest
+
+
+
+
+
+
+
+
+ <_Parameter1>$(AssemblyName).Tests
+
+
+
diff --git a/src/ReactiveMarbles.Command/RxCommand.cs b/src/ReactiveMarbles.Command/RxCommand.cs
new file mode 100644
index 0000000..5952a82
--- /dev/null
+++ b/src/ReactiveMarbles.Command/RxCommand.cs
@@ -0,0 +1,837 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.Reactive;
+using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using System.Reactive.Threading.Tasks;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using ReactiveMarbles.Locator;
+using ReactiveMarbles.Mvvm;
+
+// ReSharper disable StaticMemberInGenericType
+namespace ReactiveMarbles.Command;
+
+///
+/// Encapsulates a user action behind a reactive interface.
+///
+///
+///
+/// This non-generic base class defines the creation behavior of the RxCommand's.
+///
+///
+/// adds the concept of Input and Output generic types.
+/// The Input is often passed in by the View and it's type is captured as TInput, and the Output is
+/// the result of executing the command which type is captured as TOutput.
+///
+///
+/// is IObservable which can be used like any other IObservable.
+/// For example, you can Subscribe() to it like any other observable, and add the output to a List on your view model.
+/// The Unit type is a functional programming construct analogous to void and can be used in cases where you don't
+/// care about either the input and/or output value.
+///
+///
+/// Creating synchronous reactive commands:
+///
+/// command = RxCommand.Create(x => Console.WriteLine(x));
+///
+/// // This outputs 42 to console.
+/// command.Execute(42).Subscribe();
+///
+/// // A better approach is to invoke a command in response to an Observable.
+/// // InvokeCommand operator respects the command's executability. That is, if
+/// // the command's CanExecute method returns false, InvokeCommand will not
+/// // execute the command when the source observable ticks.
+/// Observable.Return(42).InvokeCommand(command);
+/// ]]>
+///
+///
+///
+/// Creating asynchronous reactive commands:
+///
+/// (
+/// _ => Observable.Return(42).Delay(TimeSpan.FromSeconds(2))
+/// );
+///
+/// // Calling the asynchronous reactive command:
+/// // Observable.Return(Unit.Default).InvokeCommand(command);
+///
+/// // Subscribing to values emitted by the command:
+/// command.Subscribe(Console.WriteLine);
+/// ]]>
+///
+///
+///
+public static class RxCommand
+{
+ ///
+ /// Creates a parameterless with synchronous execution logic.
+ ///
+ ///
+ /// The action to execute whenever the command is executed.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ public static RxCommand Create(
+ Action execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null) =>
+ new(_ => Observable.Create(
+ observer =>
+ {
+ execute();
+ observer.OnNext(Unit.Default);
+ observer.OnCompleted();
+ return Disposable.Empty;
+ }), canExecute, outputScheduler);
+
+ ///
+ /// Creates a with synchronous execution logic that takes a parameter of type
+ /// and returns a value of type .
+ ///
+ ///
+ /// The function to execute whenever the command is executed.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the parameter passed through to command execution.
+ ///
+ ///
+ /// The type of value returned by command executions.
+ ///
+ public static RxCommand Create(
+ Func execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null) =>
+ new(execute, canExecute, outputScheduler);
+
+ ///
+ /// Creates a with synchronous execution logic that takes a parameter of type .
+ ///
+ ///
+ /// The action to execute whenever the command is executed.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the parameter passed through to command execution.
+ ///
+ public static RxCommand Create(
+ Action execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute == null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new(param =>
+ {
+ execute(param);
+ return Unit.Default;
+ }, canExecute, outputScheduler);
+ }
+
+ ///
+ /// Creates a parameterless with synchronous execution logic that returns a value
+ /// of type .
+ ///
+ ///
+ /// The function to execute whenever the command is executed.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of value returned by command executions.
+ ///
+ public static RxCommand Create(
+ Func execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute == null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new(_ => execute(), canExecute, outputScheduler);
+ }
+
+ ///
+ /// Creates a parameterless with asynchronous execution logic.
+ ///
+ ///
+ /// Provides an observable representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the command's result.
+ ///
+ public static RxCommand Create(
+ Func> execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ _ => execute(),
+ canExecute ?? ObservableConstants.True,
+ outputScheduler);
+ }
+
+ ///
+ /// Creates a with asynchronous execution logic that takes a parameter of type .
+ ///
+ ///
+ /// Provides an observable representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the parameter passed through to command execution.
+ ///
+ ///
+ /// The type of the command's result.
+ ///
+ public static RxCommand Create(
+ Func> execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ execute,
+ canExecute ?? ObservableConstants.True,
+ outputScheduler);
+ }
+
+ ///
+ /// Creates a parameterless with asynchronous execution logic.
+ ///
+ ///
+ /// Provides a representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the command's result.
+ ///
+ public static RxCommand Create(
+ Func> execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(_ => execute(), canExecute, outputScheduler);
+ }
+
+ ///
+ /// Creates a parameterless with asynchronous execution logic.
+ ///
+ ///
+ /// Provides a representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ public static RxCommand Create(
+ Func execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ async _ =>
+ {
+ await execute().ConfigureAwait(false);
+ return Unit.Default;
+ },
+ canExecute,
+ outputScheduler);
+ }
+
+ ///
+ /// Creates a parameterless, cancellable with asynchronous execution logic.
+ ///
+ ///
+ /// Provides a representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ public static RxCommand Create(
+ Func execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ async (_, ct) =>
+ {
+ await execute(ct).ConfigureAwait(false);
+ return Unit.Default;
+ },
+ canExecute,
+ outputScheduler);
+ }
+
+ ///
+ /// Creates a with asynchronous execution logic that takes a parameter of type .
+ ///
+ ///
+ /// Provides a representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the parameter passed through to command execution.
+ ///
+ ///
+ /// The type of the command's result.
+ ///
+ public static RxCommand Create(
+ Func> execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ execute,
+ canExecute,
+ outputScheduler);
+ }
+
+ ///
+ /// Creates a with asynchronous, cancellable execution logic that takes a parameter of type .
+ ///
+ ///
+ /// Provides a representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the parameter passed through to command execution.
+ ///
+ ///
+ /// The type of the command's result.
+ ///
+ public static RxCommand Create(
+ Func> execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ execute,
+ canExecute,
+ outputScheduler);
+ }
+
+ ///
+ /// Creates a with asynchronous execution logic that takes a parameter of type .
+ ///
+ ///
+ /// Provides a representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the parameter passed through to command execution.
+ ///
+ public static RxCommand Create(
+ Func execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ param => execute(param).ToObservable(),
+ canExecute,
+ outputScheduler);
+ }
+
+ ///
+ /// Creates a with asynchronous, cancellable execution logic that takes a parameter of type .
+ ///
+ ///
+ /// Provides a representing the command's asynchronous execution logic.
+ ///
+ ///
+ /// An optional observable that dictates the availability of the command for execution.
+ ///
+ ///
+ /// An optional scheduler that is used to surface events. Defaults to RxApp.MainThreadScheduler.
+ ///
+ ///
+ /// The RxCommand instance.
+ ///
+ ///
+ /// The type of the parameter passed through to command execution.
+ ///
+ public static RxCommand Create(
+ Func execute,
+ IObservable? canExecute = null,
+ IScheduler? outputScheduler = null)
+ {
+ if (execute is null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+
+ return new RxCommand(
+ async (param, ct) =>
+ {
+ await execute(param, ct).ConfigureAwait(false);
+ return Unit.Default;
+ },
+ canExecute,
+ outputScheduler);
+ }
+}
+
+///
+/// Encapsulates a user interaction behind a reactive interface.
+///
+///
+/// The type of parameter values passed in during command execution.
+///
+///
+/// The type of the values that are the result of command execution.
+///
+public class RxCommand : IRxCommand
+{
+ private readonly IObservable _canExecute;
+ private readonly IDisposable _canExecuteSubscription;
+ private readonly ScheduledSubject _exceptions;
+ private readonly Func> _execute;
+ private readonly Subject _executionInfo;
+ private readonly IObservable _isExecuting;
+ private readonly IObservable _results;
+ private readonly ISubject _synchronizedExecutionInfo;
+ private EventHandler? _canExecuteChanged;
+ private bool _canExecuteValue;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Func to perform when the command is executed.
+ /// A observable which has a value if the command can execute.
+ /// The scheduler where to send output after the main execution.
+ /// Thrown if any dependent parameters are null.
+ public RxCommand(
+ Func execute,
+ IObservable? canExecute,
+ IScheduler? outputScheduler = null)
+ : this(
+ param => Observable.Create(observer =>
+ {
+ observer.OnNext(execute(param));
+ observer.OnCompleted();
+ return Disposable.Empty;
+ }),
+ canExecute,
+ outputScheduler)
+ {
+ if (execute == null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Func to perform when the command is executed.
+ /// A observable which has a value if the command can execute.
+ /// The scheduler where to send output after the main execution.
+ /// Thrown if any dependent parameters are null.
+ public RxCommand(
+ Func> execute,
+ IObservable? canExecute,
+ IScheduler? outputScheduler = null)
+ : this(param => Observable.FromAsync(() => execute(param)), canExecute, outputScheduler)
+ {
+ if (execute == null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Func to perform when the command is executed.
+ /// A observable which has a value if the command can execute.
+ /// The scheduler where to send output after the main execution.
+ /// Thrown if any dependent parameters are null.
+ public RxCommand(
+ Func> execute,
+ IObservable? canExecute,
+ IScheduler? outputScheduler = null)
+ : this(param => Observable.FromAsync(ct => execute(param, ct)), canExecute, outputScheduler)
+ {
+ if (execute == null)
+ {
+ throw new ArgumentNullException(nameof(execute));
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The Func to perform when the command is executed.
+ /// A observable which has a value if the command can execute.
+ /// The scheduler where to send output after the main execution.
+ /// Thrown if any dependent parameters are null.
+ public RxCommand(
+ Func> execute,
+ IObservable? canExecute,
+ IScheduler? outputScheduler = null)
+ {
+ canExecute ??= ObservableConstants.True;
+
+ _execute = execute ?? throw new ArgumentNullException(nameof(execute));
+ var coreRegistration = ServiceLocator.Current().GetService();
+ var scheduler = outputScheduler ?? coreRegistration.MainThreadScheduler;
+ _exceptions = new ScheduledSubject(scheduler, coreRegistration.ExceptionHandler);
+ _executionInfo = new Subject();
+ _synchronizedExecutionInfo = Subject.Synchronize(_executionInfo, scheduler);
+ _isExecuting = _synchronizedExecutionInfo.Scan(
+ 0,
+ (acc, next) =>
+ {
+ return next.Demarcation switch
+ {
+ ExecutionDemarcation.Begin => acc + 1,
+ ExecutionDemarcation.End => acc - 1,
+ _ => acc
+ };
+ }).Select(inFlightCount => inFlightCount > 0)
+ .StartWith(false)
+ .DistinctUntilChanged()
+ .Replay(1)
+ .RefCount();
+
+ _canExecute = canExecute.Catch(
+ ex =>
+ {
+ _exceptions.OnNext(ex);
+ return ObservableConstants.False;
+ }).StartWith(false)
+ .CombineLatest(_isExecuting, (canEx, isEx) => canEx && !isEx)
+ .DistinctUntilChanged()
+ .Replay(1)
+ .RefCount();
+
+ _results = _synchronizedExecutionInfo.Where(x => x.Demarcation == ExecutionDemarcation.Result)
+ .Select(x => x.Result);
+
+ _canExecuteSubscription = _canExecute
+ .Subscribe(OnCanExecuteChanged);
+ }
+
+ ///
+ event EventHandler? ICommand.CanExecuteChanged
+ {
+ add => _canExecuteChanged += value;
+ remove => _canExecuteChanged -= value;
+ }
+
+ private enum ExecutionDemarcation
+ {
+ Begin,
+
+ Result,
+
+ End
+ }
+
+ ///
+ public IObservable CanCommandExecute => _canExecute;
+
+ ///
+ public IObservable IsExecuting => _isExecuting;
+
+ ///
+ public IObservable ThrownExceptions => _exceptions.AsObservable();
+
+ ///
+ bool ICommand.CanExecute(object? parameter) => ICommandCanExecute(parameter);
+
+ ///
+ void ICommand.Execute(object? parameter) => ICommandExecute(parameter);
+
+ ///
+ public IObservable Execute(TParam parameter)
+ {
+ try
+ {
+ return Observable.Defer(
+ () =>
+ {
+ _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateBegin());
+ return ObservableConstants.Empty;
+ }).Concat(_execute(parameter))
+ .Do(result => _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateResult(result)))
+ .Catch(
+ ex =>
+ {
+ _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateEnd());
+ _exceptions.OnNext(ex);
+ return Observable.Throw(ex);
+ }).Finally(() => _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateEnd()))
+ .PublishLast()
+ .RefCount();
+ }
+ catch (Exception ex)
+ {
+ _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateEnd());
+ _exceptions.OnNext(ex);
+ return Observable.Throw(ex);
+ }
+ }
+
+ ///
+ public IObservable Execute()
+ {
+ try
+ {
+ return Observable.Defer(
+ () =>
+ {
+ _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateBegin());
+ return ObservableConstants.Empty;
+ }).Concat(_execute(default!))
+ .Do(result => _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateResult(result)))
+ .Catch(
+ ex =>
+ {
+ _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateEnd());
+ _exceptions.OnNext(ex);
+ return Observable.Throw(ex);
+ }).Finally(() => _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateEnd()))
+ .PublishLast()
+ .RefCount();
+ }
+ catch (Exception ex)
+ {
+ _synchronizedExecutionInfo.OnNext(ExecutionInfo.CreateEnd());
+ _exceptions.OnNext(ex);
+ return Observable.Throw(ex);
+ }
+ }
+
+ ///
+ public IDisposable Subscribe(IObserver observer) => _results.Subscribe(observer);
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ ///
+ /// Will trigger a event when the CanExecute condition has changed.
+ ///
+ /// The new value of the execute.
+ protected virtual void OnCanExecuteChanged(bool newValue)
+ {
+ _canExecuteValue = newValue;
+ _canExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ /// Dispose of the members that are disposable.
+ ///
+ /// A value that indicates whether or not is is being disposed by the dispose method.
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!disposing)
+ {
+ return;
+ }
+
+ _executionInfo.Dispose();
+ _exceptions.Dispose();
+ _canExecuteSubscription.Dispose();
+ }
+
+ ///
+ /// Will be called by the methods from the ICommand interface.
+ /// This method is called when the Command should evaluate if it can execute.
+ ///
+ /// The parameter being passed to the ICommand.
+ /// If the command can be executed.
+ protected virtual bool ICommandCanExecute(object? parameter) => _canExecuteValue;
+
+ ///
+ /// Will be called by the methods from the ICommand interface.
+ /// This method is called when the Command should execute.
+ ///
+ /// The parameter being passed to the ICommand.
+ protected virtual void ICommandExecute(object? parameter)
+ {
+ // ensure that null is coerced to default(TParam) so that commands taking value types will use a sensible default if no parameter is supplied
+ parameter ??= default(TParam);
+
+ if (parameter is not null && !(parameter is TParam))
+ {
+ throw new InvalidOperationException(
+ $"Command requires parameters of type {typeof(TParam).FullName}, but received parameter of type {parameter.GetType().FullName}.");
+ }
+
+ var result = parameter is null ? Execute() : Execute((TParam)parameter);
+
+ result
+ .Catch(ObservableConstants.Empty)
+ .Subscribe();
+ }
+
+ private readonly struct ExecutionInfo
+ {
+ private ExecutionInfo(ExecutionDemarcation demarcation, TResult result)
+ {
+ Demarcation = demarcation;
+ Result = result;
+ }
+
+ public ExecutionDemarcation Demarcation { get; }
+
+ public TResult Result { get; }
+
+ public static ExecutionInfo CreateBegin() => new(ExecutionDemarcation.Begin, default!);
+
+ public static ExecutionInfo CreateResult(TResult result) =>
+ new(ExecutionDemarcation.Result, result);
+
+ public static ExecutionInfo CreateEnd() => new(ExecutionDemarcation.End, default!);
+ }
+}
diff --git a/src/ReactiveMarbles.Command/RxCommandExtensions.cs b/src/ReactiveMarbles.Command/RxCommandExtensions.cs
new file mode 100644
index 0000000..356c101
--- /dev/null
+++ b/src/ReactiveMarbles.Command/RxCommandExtensions.cs
@@ -0,0 +1,221 @@
+// Copyright (c) 2019-2022 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System;
+using System.ComponentModel;
+using System.Linq.Expressions;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Windows.Input;
+using ReactiveMarbles.PropertyChanged;
+
+namespace ReactiveMarbles.Command;
+
+///
+/// Extension method for invoking commands.
+///
+public static class RxCommandExtensions
+{
+ ///
+ /// A utility method that will pipe an Observable to an ICommand (i.e.
+ /// it will first call its CanExecute with the provided value, then if
+ /// the command can be executed, Execute() will be called).
+ ///
+ /// The type.
+ /// The source observable to pipe into the command.
+ /// The command to be executed.
+ /// An object that when disposes, disconnects the Observable
+ /// from the command.
+ public static IDisposable InvokeCommand(this IObservable item, ICommand? command)
+ {
+ var canExecuteChanged = Observable.FromEvent(
+ eventHandler =>
+ {
+ void Handler(object? sender, EventArgs e) => eventHandler(Unit.Default);
+ return Handler;
+ },
+ h => command!.CanExecuteChanged += h,
+ h => command!.CanExecuteChanged -= h)
+ .StartWith(Unit.Default);
+
+ return WithLatestFromFixed(item, canExecuteChanged, (value, _) => new InvokeCommandInfo(command, command!.CanExecute(value), value))
+ .Where(ii => ii.CanExecute)
+ .Do(ii => command?.Execute(ii.Value))
+ .Subscribe();
+ }
+
+ ///
+ /// A utility method that will pipe an Observable to an ICommand (i.e.
+ /// it will first call its CanExecute with the provided value, then if
+ /// the command can be executed, Execute() will be called).
+ ///
+ /// The type.
+ /// The result type.
+ /// The source observable to pipe into the command.
+ /// The command to be executed.
+ /// An object that when disposes, disconnects the Observable
+ /// from the command.
+ public static IDisposable InvokeCommand(this IObservable item, RxCommand? command) =>
+ command is null
+ ? throw new ArgumentNullException(nameof(command))
+ : WithLatestFromFixed(item, command.CanCommandExecute, (value, canExecute) => new InvokeCommandInfo, T>(command, canExecute, value))
+ .Where(ii => ii.CanExecute)
+ .SelectMany(ii => command.Execute(ii.Value).Catch(ObservableConstants.Empty))
+ .Subscribe();
+
+ ///
+ /// A utility method that will pipe an Observable to an ICommand (i.e.
+ /// it will first call its CanExecute with the provided value, then if
+ /// the command can be executed, Execute() will be called).
+ ///
+ /// The type.
+ /// The target type.
+ /// The source observable to pipe into the command.
+ /// The root object which has the Command.
+ /// The expression to reference the Command.
+ /// An object that when disposes, disconnects the Observable
+ /// from the command.
+ public static IDisposable InvokeCommand(this IObservable item, TTarget? target, Expression> commandProperty)
+ where TTarget : class, INotifyPropertyChanged
+ {
+ if (commandProperty == null)
+ {
+ throw new ArgumentNullException(nameof(commandProperty));
+ }
+
+ var commandObs =
+ target.WhenChanged(commandProperty!)
+ .Where(x => x != null)
+ .Select(x => x!);
+ var commandCanExecuteChanged = commandObs
+ .Select(command => command is null
+ ? ObservableConstants.Empty
+ : Observable
+ .FromEvent(
+ eventHandler => (_, _) => eventHandler(command),
+ h => command.CanExecuteChanged += h,
+ h => command.CanExecuteChanged -= h)
+ .StartWith(command))
+ .Switch();
+
+ return WithLatestFromFixed(
+ item,
+ commandCanExecuteChanged,
+ (value, cmd) => new InvokeCommandInfo(cmd, cmd.CanExecute(value), value))
+ .Where(ii => ii.CanExecute)
+ .Do(ii => ii.Command.Execute(ii.Value))
+ .Subscribe();
+ }
+
+ ///
+ /// A utility method that will pipe an Observable to an ICommand (i.e.
+ /// it will first call its CanExecute with the provided value, then if
+ /// the command can be executed, Execute() will be called).
+ ///
+ /// The type.
+ /// The result type.
+ /// The target type.
+ /// The source observable to pipe into the command.
+ /// The root object which has the Command.
+ /// The expression to reference the Command.
+ /// An object that when disposes, disconnects the Observable
+ /// from the command.
+ public static IDisposable InvokeCommand(
+ this IObservable item,
+ TTarget? target,
+ Expression?>> commandProperty)
+ where TTarget : class, INotifyPropertyChanged
+ {
+ if (commandProperty == null)
+ {
+ throw new ArgumentNullException(nameof(commandProperty));
+ }
+
+ var command =
+ target.WhenChanged(commandProperty!)
+ .Where(x => x != null)
+ .Select(x => x!);
+ var invocationInfo = command
+ .Select(cmd => cmd is null
+ ? ObservableConstants, T>>.Empty
+ : cmd
+ .CanCommandExecute
+ .Select(canExecute => new InvokeCommandInfo, T>(cmd, canExecute)))
+ .Switch();
+
+ return WithLatestFromFixed(item, invocationInfo, (value, ii) => ii.WithValue(value))
+ .Where(ii => ii.CanExecute)
+ .SelectMany(ii => ii.Command.Execute(ii.Value).Catch(ObservableConstants.Empty))
+ .Subscribe();
+ }
+
+ ///
+ /// A utility method that will pipe an IRxCommand from an Observable (i.e.
+ /// it will first call its CanExecute with the provided value, then if
+ /// the command can be executed, Execute() will be called).
+ ///
+ /// The type.
+ /// The result type.
+ /// The source observable to pipe into the command.
+ /// An object that when disposes, disconnects the Observable
+ /// from the command.
+ public static IDisposable InvokeCommand(this IObservable source)
+ where TParam : IRxCommand
+ {
+ var command = source.Where(x => x != null).Select(x => x!);
+ var invocationInfo = command
+ .Select(cmd => cmd is null
+ ? ObservableConstants, TParam>>.Empty
+ : cmd
+ .CanCommandExecute
+ .Select(canExecute => new InvokeCommandInfo, TParam>(cmd, canExecute)))
+ .Switch();
+
+ return WithLatestFromFixed(source, invocationInfo, (value, ii) => ii.WithValue(value))
+ .Where(ii => ii.CanExecute)
+ .SelectMany(ii => ii.Command.Execute(ii.Value).Catch(ObservableConstants.Empty))
+ .Subscribe();
+ }
+
+ // See https://github.com/Reactive-Extensions/Rx.NET/issues/444
+ private static IObservable WithLatestFromFixed(
+ IObservable item,
+ IObservable other,
+ Func resultSelector) =>
+ item
+ .Publish(
+ os =>
+ other
+ .Select(
+ a =>
+ os
+ .Select(b => resultSelector(b, a)))
+ .Switch());
+
+ private readonly struct InvokeCommandInfo
+ {
+ public InvokeCommandInfo(TCommand command, bool canExecute, TValue value)
+ {
+ Command = command;
+ CanExecute = canExecute;
+ Value = value!;
+ }
+
+ public InvokeCommandInfo(TCommand command, bool canExecute)
+ {
+ Command = command;
+ CanExecute = canExecute;
+ Value = default!;
+ }
+
+ public TCommand Command { get; }
+
+ public bool CanExecute { get; }
+
+ public TValue Value { get; }
+
+ public InvokeCommandInfo WithValue(TValue value) =>
+ new(Command, CanExecute, value);
+ }
+}
diff --git a/src/directory.build.props b/src/directory.build.props
new file mode 100644
index 0000000..b2cf7f8
--- /dev/null
+++ b/src/directory.build.props
@@ -0,0 +1,65 @@
+
+
+ ReactiveUI Association Inc
+ ReactiveUI Association Inc
+ Copyright (c) ReactiveUI Association Inc © $([System.DateTime]::Now.ToString('yyyy'))
+ MIT
+ https://github.com/reactivemarbles/Mvvm
+ Common base classes for the MVVM pattern for Reactive Marbles.
+ logo.png
+ glennawatson;rlittlesii
+ system.reactive;propertychanged;inpc;reactive;functional;mvvm
+ https://github.com/reactivemarbles/Mvvm/releases
+ https://github.com/reactivemarbles/Mvvm
+ git
+ true
+ enable
+ true
+ true
+ AnyCPU
+ $(MSBuildProjectName.Contains('Tests'))
+ embedded
+
+
+ true
+
+ true
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+
+
+
+
+ $(MSBuildThisFileDirectory)
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
diff --git a/src/directory.build.targets b/src/directory.build.targets
new file mode 100644
index 0000000..61eaf0d
--- /dev/null
+++ b/src/directory.build.targets
@@ -0,0 +1,44 @@
+
+
+
+
+ $(AssemblyName) ($(TargetFramework))
+ False
+ false
+
+
+
+ $(DefineConstants);NETSTANDARD;PORTABLE
+
+
+ $(DefineConstants);NET_461;XAML
+
+
+ $(DefineConstants);NETFX_CORE;XAML;WINDOWS;WINDOWS_UWP
+
+
+ $(DefineConstants);MONO;UIKIT;COCOA;IOS
+
+
+ $(DefineConstants);MONO;COCOA;MAC
+
+
+ $(DefineConstants);MONO;UIKIT;COCOA;TVOS
+
+
+ $(DefineConstants);MONO;COCOA;WATCHOS
+
+
+ $(DefineConstants);MONO;ANDROID
+ false
+
+
+ $(DefineConstants);NETCOREAPP
+
+
+ $(DefineConstants);TIZEN
+
+
+
+
+
diff --git a/src/directory.packages.props b/src/directory.packages.props
new file mode 100644
index 0000000..76a221b
--- /dev/null
+++ b/src/directory.packages.props
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/stylecop.json b/src/stylecop.json
new file mode 100644
index 0000000..038eadf
--- /dev/null
+++ b/src/stylecop.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "indentation": {
+ "useTabs": false,
+ "indentationSize": 4
+ },
+ "documentationRules": {
+ "documentExposedElements": true,
+ "documentInternalElements": false,
+ "documentPrivateElements": false,
+ "documentInterfaces": true,
+ "documentPrivateFields": false,
+ "documentationCulture": "en-US",
+ "companyName": "ReactiveUI Association Incorporated",
+ "copyrightText": "Copyright (c) 2019-2022 {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.",
+ "variables": {
+ "licenseName": "MIT",
+ "licenseFile": "LICENSE"
+ },
+ "xmlHeader": false
+ },
+ "layoutRules": {
+ "newlineAtEndOfFile": "allow",
+ "allowConsecutiveUsings": true
+ },
+ "maintainabilityRules": {
+ "topLevelTypes": [
+ "class",
+ "interface",
+ "struct",
+ "enum",
+ "delegate"
+ ]
+ },
+ "orderingRules": {
+ "usingDirectivesPlacement": "outsideNamespace",
+ "systemUsingDirectivesFirst": true
+ }
+ }
+}
diff --git a/version.json b/version.json
new file mode 100644
index 0000000..75b0c44
--- /dev/null
+++ b/version.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
+ "version": "1.0",
+ "publicReleaseRefSpec": [
+ "^refs/heads/master$", // we release out of master
+ "^refs/heads/main$",
+ "^refs/heads/latest$",
+ "^refs/heads/preview/.*", // we release previews
+ "^refs/heads/rel/\\d+\\.\\d+\\.\\d+" // we also release branches starting with rel/N.N.N
+ ],
+ "nugetPackageVersion":{
+ "semVer": 2
+ },
+ "cloudBuild": {
+ "setVersionVariables": true,
+ "buildNumber": {
+ "enabled": false
+ }
+ }
+}
From 3f2971d58576fdfb32a2f0a666c1f59acb51d739 Mon Sep 17 00:00:00 2001
From: rlittlesii <6969701+RLittlesII@users.noreply.github.com>
Date: Mon, 18 Apr 2022 20:27:11 -0500
Subject: [PATCH 2/3] leaving bread crumbs
---
src/ReactiveMarbles.Command.Tests/RxCommandTests.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs b/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
index 9abf3a5..08a081b 100644
--- a/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
+++ b/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
@@ -1256,8 +1256,10 @@ async Task Execute()
throw new Exception("break execution");
}
+ // TODO: [rlittlesii: April 18, 2022] This fails
var fixture = RxCommand.Create(Execute);
+ // this passes
var fixture = new RxCommand(
async _ =>
{
From 65fc92c452f8e862caf7a1749da37a757e6e1ba4 Mon Sep 17 00:00:00 2001
From: Chris Pulman
Date: Mon, 13 Feb 2023 23:28:23 +0000
Subject: [PATCH 3/3] Add OnCompleted to Dispose of RxCommand
---
.github/dependabot.yml | 13 +++
.github/workflows/ci-build.yml | 83 ++++++++++++++++
.github/workflows/lock.yml | 31 ++++++
.github/workflows/release.yml | 98 +++++++++++++++++++
.../RxCommandTests.cs | 62 +++++++++---
src/ReactiveMarbles.Command.sln | 24 ++++-
.../ReactiveMarbles.Command.csproj | 2 +-
src/ReactiveMarbles.Command/RxCommand.cs | 15 ++-
8 files changed, 313 insertions(+), 15 deletions(-)
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/ci-build.yml
create mode 100644 .github/workflows/lock.yml
create mode 100644 .github/workflows/release.yml
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..9d06ed1
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+- package-ecosystem: nuget
+ directory: "/"
+ schedule:
+ interval: monthly
+ time: "00:00"
+ open-pull-requests-limit: 20
+- package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ # Check for updates to GitHub Actions every weekday
+ interval: "monthly"
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
new file mode 100644
index 0000000..ab940ad
--- /dev/null
+++ b/.github/workflows/ci-build.yml
@@ -0,0 +1,83 @@
+name: Build
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+
+env:
+ configuration: Release
+ productNamespacePrefix: "ReactiveMarbles"
+
+jobs:
+ build:
+ runs-on: windows-2022
+ outputs:
+ nbgv: ${{ steps.nbgv.outputs.SemVer2 }}
+ steps:
+ - name: Get Current Visual Studio Information
+ shell: bash
+ run: |
+ dotnet tool update -g dotnet-vs
+ echo "-- About RELEASE --"
+ vs where release
+
+ - name: Update Visual Studio Latest Release
+ shell: bash
+ run: |
+ echo "-- Update RELEASE --"
+ vs update release Enterprise
+ vs modify release Enterprise +mobile +desktop +uwp +web
+ echo "-- About RELEASE Updated --"
+ vs where release
+
+ - name: Checkout
+ uses: actions/checkout@v3.2.0
+ with:
+ fetch-depth: 0
+ lfs: true
+
+ - name: Install .NET 6 & 7
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+
+ - name: NBGV
+ id: nbgv
+ uses: dotnet/nbgv@master
+ with:
+ setAllVars: true
+
+ - name: NuGet Restore
+ run: dotnet restore
+ working-directory: src
+
+ - name: Build
+ run: dotnet build --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ - name: Run Unit Tests and Generate Coverage
+ uses: glennawatson/coverlet-msbuild@v2.1
+ with:
+ project-files: '**/*Tests*.csproj'
+ no-build: true
+ exclude-filter: '[${{env.productNamespacePrefix}}.*.Tests.*]*'
+ include-filter: '[${{env.productNamespacePrefix}}*]*'
+ output-format: cobertura
+ configuration: ${{ env.configuration }}
+
+ - name: Pack
+ run: dotnet pack --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ - name: Upload Code Coverage
+ uses: codecov/codecov-action@v3
+
+ - name: Create NuGet Artifacts
+ uses: actions/upload-artifact@master
+ with:
+ name: nuget
+ path: '**/*.nupkg'
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
new file mode 100644
index 0000000..307b83a
--- /dev/null
+++ b/.github/workflows/lock.yml
@@ -0,0 +1,31 @@
+name: 'Lock Threads'
+
+on:
+ schedule:
+ - cron: '0 0 * * *'
+ workflow_dispatch:
+
+permissions:
+ issues: write
+ pull-requests: write
+
+concurrency:
+ group: lock
+
+jobs:
+ action:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: dessant/lock-threads@v4
+ with:
+ github-token: ${{ github.token }}
+ issue-inactive-days: '14'
+ pr-inactive-days: '14'
+ issue-comment: >
+ This issue has been automatically locked since there
+ has not been any recent activity after it was closed.
+ Please open a new issue for related bugs.
+ pr-comment: >
+ This pull request has been automatically locked since there
+ has not been any recent activity after it was closed.
+ Please open a new issue for related bugs.
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..09f282c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,98 @@
+name: Build and Release
+
+on:
+ push:
+ branches: [ main ]
+
+env:
+ configuration: Release
+ productNamespacePrefix: "ReactiveMarbles"
+
+jobs:
+ release:
+ runs-on: windows-2022
+ environment:
+ name: release
+ outputs:
+ nbgv: ${{ steps.nbgv.outputs.SemVer2 }}
+ steps:
+ - name: Get Current Visual Studio Information
+ shell: bash
+ run: |
+ dotnet tool update -g dotnet-vs
+ echo "-- About RELEASE --"
+ vs where release
+
+ - name: Update Visual Studio Latest Release
+ shell: bash
+ run: |
+ echo "-- Update RELEASE --"
+ vs update release Enterprise
+ vs modify release Enterprise +mobile +desktop +uwp +web
+ echo "-- About RELEASE Updated --"
+ vs where release
+
+ - name: Checkout
+ uses: actions/checkout@v3.2.0
+ with:
+ fetch-depth: 0
+ lfs: true
+
+ - name: Install .NET 6 & 7
+ uses: actions/setup-dotnet@v3
+ with:
+ dotnet-version: |
+ 6.0.x
+ 7.0.x
+
+ - name: NBGV
+ id: nbgv
+ uses: dotnet/nbgv@master
+ with:
+ setAllVars: true
+
+ - name: NuGet Restore
+ run: dotnet restore
+ working-directory: src
+
+ - name: Build
+ run: dotnet build --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ - uses: nuget/setup-nuget@v1
+ name: Setup NuGet
+
+ - name: Pack
+ run: dotnet pack --configuration=${{ env.configuration }} --verbosity=minimal --no-restore
+ working-directory: src
+
+ # Decode the base 64 encoded pfx and save the Signing_Certificate
+ - name: Sign NuGet packages
+ shell: pwsh
+ run: |
+ $pfx_cert_byte = [System.Convert]::FromBase64String("${{ secrets.SIGNING_CERTIFICATE }}")
+ [IO.File]::WriteAllBytes("GitHubActionsWorkflow.pfx", $pfx_cert_byte)
+ $secure_password = ConvertTo-SecureString ${{ secrets.SIGN_CERTIFICATE_PASSWORD }} –asplaintext –force
+ Import-PfxCertificate -FilePath GitHubActionsWorkflow.pfx -Password $secure_password -CertStoreLocation Cert:\CurrentUser\My
+ nuget sign -Timestamper http://timestamp.digicert.com -CertificateFingerprint ${{ secrets.SIGN_CERTIFICATE_HASH }} **/*.nupkg
+
+ - name: Changelog
+ uses: glennawatson/ChangeLog@v1
+ id: changelog
+
+ - name: Create Release
+ uses: actions/create-release@v1.1.4
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
+ with:
+ tag_name: ${{ steps.nbgv.outputs.SemVer2 }}
+ release_name: ${{ steps.nbgv.outputs.SemVer2 }}
+ body: |
+ ${{ steps.changelog.outputs.commitLog }}
+
+ - name: NuGet Push
+ env:
+ NUGET_AUTH_TOKEN: ${{ secrets.NUGET_API_KEY }}
+ SOURCE_URL: https://api.nuget.org/v3/index.json
+ run: |
+ dotnet nuget push -s ${{ env.SOURCE_URL }} -k ${{ env.NUGET_AUTH_TOKEN }} **/*.nupkg
diff --git a/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs b/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
index 08a081b..0f90600 100644
--- a/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
+++ b/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
@@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
@@ -17,6 +18,7 @@
using Microsoft.Reactive.Testing;
using ReactiveMarbles.Locator;
using ReactiveMarbles.Mvvm;
+using ReactiveUI;
using ReactiveUI.Testing;
using Xunit;
@@ -1256,18 +1258,8 @@ async Task Execute()
throw new Exception("break execution");
}
- // TODO: [rlittlesii: April 18, 2022] This fails
- var fixture = RxCommand.Create(Execute);
+ var fixture = RxCommand.Create(Execute, outputScheduler: ImmediateScheduler.Instance);
- // this passes
- var fixture = new RxCommand(
- async _ =>
- {
- await subj.Take(1);
- throw new Exception("break execution");
- },
- ObservableConstants.True,
- ImmediateScheduler.Instance);
fixture.IsExecuting.Subscribe(x => isExecuting = x);
fixture.ThrownExceptions.Subscribe(ex => fail = ex);
@@ -1314,4 +1306,52 @@ public async Task RxCommandExecutesFromInvokeCommand()
await Task.Delay(200).ConfigureAwait(false);
result.Should().Be(1);
}
+
+ ///
+ /// Tests RxCommand can dispose self.
+ ///
+ /// A representing the asynchronous unit test.
+ [Fact]
+ public async Task RxCommandCanDisposeSelf()
+ {
+ var command = RxCommand.Create(() => new object());
+ var waiter = WaitForValueAsync();
+ command.Dispose();
+ Assert.Null(await waiter.ConfigureAwait(false));
+
+ async Task