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/.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/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..0f90600
--- /dev/null
+++ b/src/ReactiveMarbles.Command.Tests/RxCommandTests.cs
@@ -0,0 +1,1357 @@
+// 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.Disposables;
+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;
+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, outputScheduler: 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);
+ }
+
+ ///
+ /// 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