diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c516004 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,527 @@ +# 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 +dotnet_diagnostic.CA1027.severity=error +dotnet_diagnostic.CA1062.severity=error +dotnet_diagnostic.CA1064.severity=error +dotnet_diagnostic.CA1066.severity=error +dotnet_diagnostic.CA1067.severity=error +dotnet_diagnostic.CA1068.severity=error +dotnet_diagnostic.CA1069.severity=warning +dotnet_diagnostic.CA2013.severity=error +dotnet_diagnostic.CA1802.severity=error +dotnet_diagnostic.CA1813.severity=error +dotnet_diagnostic.CA1814.severity=error +dotnet_diagnostic.CA1815.severity=error +dotnet_diagnostic.CA1822.severity=error +dotnet_diagnostic.CA1827.severity=error +dotnet_diagnostic.CA1828.severity=error +dotnet_diagnostic.CA1826.severity=error +dotnet_diagnostic.CA1829.severity=error +dotnet_diagnostic.CA1830.severity=error +dotnet_diagnostic.CA1831.severity=error +dotnet_diagnostic.CA1832.severity=error +dotnet_diagnostic.CA1833.severity=error +dotnet_diagnostic.CA1834.severity=error +dotnet_diagnostic.CA1835.severity=error +dotnet_diagnostic.CA1836.severity=error +dotnet_diagnostic.CA1837.severity=error +dotnet_diagnostic.CA1838.severity=error +dotnet_diagnostic.CA2015.severity=error +dotnet_diagnostic.CA2012.severity=error +dotnet_diagnostic.CA2011.severity=error +dotnet_diagnostic.CA2009.severity=error +dotnet_diagnostic.CA2008.severity=error +dotnet_diagnostic.CA2007.severity=warning +dotnet_diagnostic.CA2000.severity=suggestion + +[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 s_ 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 = s_ +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 = none +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 = none +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.RCS1102.severity=error +dotnet_diagnostic.RCS1166.severity=error +dotnet_diagnostic.RCS1078i.severity=error +dotnet_diagnostic.RCS1248.severity=suggestion +dotnet_diagnostic.RCS1080.severity=error +dotnet_diagnostic.RCS1077.severity=error +dotnet_diagnostic.CA1825.severity=error +dotnet_diagnostic.CA1812.severity=error +dotnet_diagnostic.CA1805.severity=error +dotnet_diagnostic.RCS1197.severity=error +dotnet_diagnostic.RCS1198.severity=error +dotnet_diagnostic.RCS1231.severity=none +dotnet_diagnostic.RCS1235.severity=error +dotnet_diagnostic.RCS1242.severity=error +dotnet_diagnostic.CA2016.severity=warning +dotnet_diagnostic.CA2014.severity=error +dotnet_diagnostic.RCS1010.severity=error +dotnet_diagnostic.RCS1006.severity=error +dotnet_diagnostic.RCS1005.severity=error +dotnet_diagnostic.RCS1020.severity=error +dotnet_diagnostic.RCS1049.severity=warning +dotnet_diagnostic.RCS1058.severity=warning +dotnet_diagnostic.RCS1068.severity=warning +dotnet_diagnostic.RCS1073.severity=warning +dotnet_diagnostic.RCS1084.severity=error +dotnet_diagnostic.RCS1085.severity=error +dotnet_diagnostic.RCS1105.severity=error +dotnet_diagnostic.RCS1112.severity=error +dotnet_diagnostic.RCS1128.severity=error +dotnet_diagnostic.RCS1143.severity=error +dotnet_diagnostic.RCS1171.severity=error +dotnet_diagnostic.RCS1173.severity=error +dotnet_diagnostic.RCS1176.severity=error +dotnet_diagnostic.RCS1177.severity=error +dotnet_diagnostic.RCS1179.severity=error +dotnet_diagnostic.RCS1180.severity=warning +dotnet_diagnostic.RCS1190.severity=error +dotnet_diagnostic.RCS1195.severity=error +dotnet_diagnostic.RCS1214.severity=error + +# 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..5044446 --- /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 eol=lf 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..602e8f6 --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,88 @@ +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.5.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: Install DotNet workloads + shell: bash + run: | + dotnet workload install android ios tvos macos maui maccatalyst + + - 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..be9814a --- /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.5.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/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..0d0e89f --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,57 @@ + + + Debug;Release;Design + true + $(NoWarn);1591;1701;1702;1705;VSX1000;IDE0190;IDE1006 + AnyCPU + $(MSBuildProjectName.Contains('Tests')) + embedded + Chris Pulman, Glenn Watson + Copyright (c) $([System.DateTime]::Now.ToString(yyyy)) ReactiveUI Association Inc + MIT + https://github.com/reactivemarbles/Navigation + Provides a Navigation Framework for ReactiveMarbles based projects. + logo.png + README.md + chrispulman;glennawatson + Navigation;inpc;reactive;functional + https://github.com/reactivemarbles/Navigation/releases + https://github.com/reactivemarbles/Navigation + git + true + + + true + + true + + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + $(MSBuildThisFileDirectory) + + + + true + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ReactiveMarbles.Navigation.sln b/src/ReactiveMarbles.Navigation.sln new file mode 100644 index 0000000..86e10b7 --- /dev/null +++ b/src/ReactiveMarbles.Navigation.sln @@ -0,0 +1,100 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33530.505 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.ViewModel.Wpf", "ViewModel.Wpf\ReactiveMarbles.ViewModel.Wpf.csproj", "{6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.ViewModel.Core", "ViewModel.Core\ReactiveMarbles.ViewModel.Core.csproj", "{990C0E6D-0C09-482D-8AFA-F135EC206C43}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.ViewModel.WinForms", "ViewModel.WinForms\ReactiveMarbles.ViewModel.WinForms.csproj", "{CD830D09-4558-403B-9504-C0CFD90FDC3B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionConfig", "SolutionConfig", "{B9D5E36A-C2F2-4D70-B5D0-797574468968}" + ProjectSection(SolutionItems) = preProject + ..\.editorconfig = ..\.editorconfig + ..\.gitattributes = ..\.gitattributes + ..\.gitignore = ..\.gitignore + ..\.github\workflows\ci-build.yml = ..\.github\workflows\ci-build.yml + Directory.Build.props = Directory.Build.props + ..\LICENSE = ..\LICENSE + ..\README.md = ..\README.md + ..\.github\workflows\release.yml = ..\.github\workflows\release.yml + stylecop.json = stylecop.json + ..\version.json = ..\version.json + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{6E654C37-7F3A-4E17-83C1-CA8BC9B54476}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViewModel.Wpf.Example", "ViewModel.Wpf.Example\ViewModel.Wpf.Example.csproj", "{5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ViewModel.WinForms.Example", "ViewModel.WinForms.Example\ViewModel.WinForms.Example.csproj", "{67688407-5509-4158-B9C3-5C2076E4946C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveMarbles.ViewModel.MAUI", "ReactiveMarbles.ViewModel.MAUI\ReactiveMarbles.ViewModel.MAUI.csproj", "{267783AB-7239-42A1-9596-22F53B8884D1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ViewModel.MAUI.Example", "ViewModel.MAUI.Example\ViewModel.MAUI.Example.csproj", "{DE39BF42-E055-4797-9807-6AAA44089FE5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Design|Any CPU = Design|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610}.Design|Any CPU.ActiveCfg = Design|Any CPU + {6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610}.Design|Any CPU.Build.0 = Design|Any CPU + {6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B5A20B7-CAA7-4CBF-B48F-6EFFE9414610}.Release|Any CPU.Build.0 = Release|Any CPU + {990C0E6D-0C09-482D-8AFA-F135EC206C43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {990C0E6D-0C09-482D-8AFA-F135EC206C43}.Debug|Any CPU.Build.0 = Debug|Any CPU + {990C0E6D-0C09-482D-8AFA-F135EC206C43}.Design|Any CPU.ActiveCfg = Design|Any CPU + {990C0E6D-0C09-482D-8AFA-F135EC206C43}.Design|Any CPU.Build.0 = Design|Any CPU + {990C0E6D-0C09-482D-8AFA-F135EC206C43}.Release|Any CPU.ActiveCfg = Release|Any CPU + {990C0E6D-0C09-482D-8AFA-F135EC206C43}.Release|Any CPU.Build.0 = Release|Any CPU + {CD830D09-4558-403B-9504-C0CFD90FDC3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD830D09-4558-403B-9504-C0CFD90FDC3B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD830D09-4558-403B-9504-C0CFD90FDC3B}.Design|Any CPU.ActiveCfg = Design|Any CPU + {CD830D09-4558-403B-9504-C0CFD90FDC3B}.Design|Any CPU.Build.0 = Design|Any CPU + {CD830D09-4558-403B-9504-C0CFD90FDC3B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD830D09-4558-403B-9504-C0CFD90FDC3B}.Release|Any CPU.Build.0 = Release|Any CPU + {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E}.Design|Any CPU.ActiveCfg = Design|Any CPU + {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E}.Design|Any CPU.Build.0 = Design|Any CPU + {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E}.Release|Any CPU.Build.0 = Release|Any CPU + {67688407-5509-4158-B9C3-5C2076E4946C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {67688407-5509-4158-B9C3-5C2076E4946C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {67688407-5509-4158-B9C3-5C2076E4946C}.Design|Any CPU.ActiveCfg = Design|Any CPU + {67688407-5509-4158-B9C3-5C2076E4946C}.Design|Any CPU.Build.0 = Design|Any CPU + {67688407-5509-4158-B9C3-5C2076E4946C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {67688407-5509-4158-B9C3-5C2076E4946C}.Release|Any CPU.Build.0 = Release|Any CPU + {267783AB-7239-42A1-9596-22F53B8884D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {267783AB-7239-42A1-9596-22F53B8884D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {267783AB-7239-42A1-9596-22F53B8884D1}.Design|Any CPU.ActiveCfg = Design|Any CPU + {267783AB-7239-42A1-9596-22F53B8884D1}.Design|Any CPU.Build.0 = Design|Any CPU + {267783AB-7239-42A1-9596-22F53B8884D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {267783AB-7239-42A1-9596-22F53B8884D1}.Release|Any CPU.Build.0 = Release|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Design|Any CPU.ActiveCfg = Design|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Design|Any CPU.Build.0 = Design|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Design|Any CPU.Deploy.0 = Design|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Release|Any CPU.Build.0 = Release|Any CPU + {DE39BF42-E055-4797-9807-6AAA44089FE5}.Release|Any CPU.Deploy.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {5E67BC1D-9AD7-4DDA-B095-6EFE99AD2C2E} = {6E654C37-7F3A-4E17-83C1-CA8BC9B54476} + {67688407-5509-4158-B9C3-5C2076E4946C} = {6E654C37-7F3A-4E17-83C1-CA8BC9B54476} + {DE39BF42-E055-4797-9807-6AAA44089FE5} = {6E654C37-7F3A-4E17-83C1-CA8BC9B54476} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {489B52FF-BC1B-435B-8412-301C8A46D473} + EndGlobalSection +EndGlobal diff --git a/src/ReactiveMarbles.ViewModel.MAUI/CoreRegistrationBuilderMixins.cs b/src/ReactiveMarbles.ViewModel.MAUI/CoreRegistrationBuilderMixins.cs new file mode 100644 index 0000000..06c49ee --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/CoreRegistrationBuilderMixins.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2019-2023 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.Concurrency; +using ReactiveMarbles.Mvvm; + +namespace ReactiveMarbles.Locator; + +/// +/// CoreRegistrationBuilderMixins. +/// +public static class CoreRegistrationBuilderMixins +{ + /// + /// Uses the WPF thread schedulers. + /// + /// The builder. + /// The Builder. + /// builder. + public static CoreRegistrationBuilder UseMauiThreadSchedulers(this CoreRegistrationBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + +#if WINUI + builder.WithMainThreadScheduler(new MauiDispatcherScheduler(() => MauiWinUIScheduler.Current)); +#elif MAUIANDROID + builder.WithMainThreadScheduler(MauiAndroidScheduler.MainThreadScheduler); +#elif MAUIMAC + builder.WithMainThreadScheduler(new MauiDispatcherScheduler(() => new MauiMacScheduler())); +#endif + return builder.WithTaskPoolScheduler(TaskPoolScheduler.Default); + } +} diff --git a/src/ReactiveMarbles.ViewModel.MAUI/NavigationShell.cs b/src/ReactiveMarbles.ViewModel.MAUI/NavigationShell.cs new file mode 100644 index 0000000..b439586 --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/NavigationShell.cs @@ -0,0 +1,504 @@ +// Copyright (c) 2019-2023 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.Collections.ObjectModel; +using System.Diagnostics; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using DynamicData; +using ReactiveMarbles.Locator; +using ReactiveMarbles.Mvvm; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.ViewModel.MAUI; + +/// +/// NavigationShell. +/// +public class NavigationShell : Shell, ISetNavigation, IViewModelRoutedViewHost, IUseNavigation +{ + /// + /// The navigate back is enabled property. + /// + public static readonly BindableProperty CanNavigateBackProperty = BindableProperty.Create( + nameof(CanNavigateBack), + typeof(bool), + typeof(NavigationShell), + false); + + /// + /// The host name property. + /// + public static readonly BindableProperty NameProperty = BindableProperty.Create( + nameof(Name), + typeof(string), + typeof(NavigationShell), + string.Empty, + BindingMode.Default, + propertyChanged: NameChanged); + + /// + /// The navigate back is enabled property. + /// + public static readonly BindableProperty NavigateBackIsEnabledProperty = BindableProperty.Create( + nameof(NavigateBackIsEnabled), + typeof(bool), + typeof(NavigationShell), + true); + + private readonly ISubject _canNavigateBackSubject = new Subject(); + private readonly ISubject _currentViewModel = new Subject(); + private IRxNavBase? __currentViewModel; + private IAmViewFor? _currentView; + private IAmViewFor? _lastView; + private bool _navigateBack; + private bool _resetStack; + private IRxNavBase? _toViewModel; + private bool _userInstigated; + private ICoreRegistration? _coreRegistration; + + /// + /// Initializes a new instance of the class. + /// + public NavigationShell() => + CurrentViewModel.Subscribe(vm => + { + if (vm is IRxNavBase rxo && _userInstigated) + { + __currentViewModel = rxo; + if (!_navigateBack) + { + NavigationStack.Add(__currentViewModel); + } + else + { + // Navigate Back + if (NavigationStack?.Count > 1) + { + NavigationStack.Remove(NavigationStack.Last()); + } + } + } + + if (_currentView != null) + { + GotoPage(); + } + + _navigateBack = false; + + CanNavigateBack = NavigationStack?.Count > 1; + _canNavigateBackSubject.OnNext(CanNavigateBack); + }); + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// true if [navigate back is enabled]; otherwise, false. + public bool CanNavigateBack + { + get => (bool)GetValue(CanNavigateBackProperty); + set => SetValue(CanNavigateBackProperty, value); + } + + /// + /// Gets the can navigate back observable. + /// + /// + /// The can navigate back observable. + /// + public IObservable CanNavigateBackObservable => _canNavigateBackSubject; + + /// + /// Gets the current view model. + /// + /// + /// The current view model. + /// + public IObservable CurrentViewModel => _currentViewModel.Publish().RefCount(); + + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + public string Name + { + get => (string)GetValue(NameProperty); + set => SetValue(NameProperty, value); + } + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// + /// true if [navigate back is enabled]; otherwise, false. + /// + public bool NavigateBackIsEnabled + { + get => (bool)GetValue(NavigateBackIsEnabledProperty); + set => SetValue(NavigateBackIsEnabledProperty, value); + } + + /// + /// Gets the navigation stack. + /// + /// + /// The navigation stack. + /// + public ObservableCollection NavigationStack { get; } = new(); + + /// + /// Gets a value indicating whether [requires setup]. + /// + /// + /// true if [requires setup]; otherwise, false. + /// + public bool RequiresSetup => true; + + /// + /// Clears the history. + /// + public void ClearHistory() => NavigationStack.Clear(); + + /// + /// Navigates the ViewModel contract. + /// + /// The Type. + /// The contract. + /// The parameter. + public void Navigate(string? contract = null, object? parameter = null) + where T : class, IRxNavBase => InternalNavigate(contract, parameter); + + /// + /// Navigates the specified contract. + /// + /// The view model. + /// The parameter. + public void Navigate(IRxNavBase viewModel, object? parameter = null) + => InternalNavigate(viewModel, parameter); + + /// + /// Navigates and resets. + /// + /// The Type. + /// The contract. + /// The parameter. + public void NavigateAndReset(string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + _resetStack = true; + InternalNavigate(contract, parameter); + } + + /// + /// Navigates the and reset. + /// + /// The view model. + /// The parameter. + public void NavigateAndReset(IRxNavBase viewModel, object? parameter = null) + { + _resetStack = true; + InternalNavigate(viewModel, parameter); + } + + /// + /// Navigates back. + /// + /// The parameter. + public void NavigateBack(object? parameter = null) + { + if (NavigateBackIsEnabled && CanNavigateBack && NavigationStack.Count > 1) + { + _userInstigated = true; + _navigateBack = true; + + // Get the previous View + var count = NavigationStack.Count; + _toViewModel = NavigationStack[count - 2]; + + var ea = new ViewModelNavigatingEventArgs(__currentViewModel, _toViewModel, NavigationType.Back, _lastView, Name, parameter); + if (_currentView is INotifiyNavigation { ISetupNavigating: true }) + { + ViewModelRoutedViewHostMixins.SetWhenNavigating.OnNext(ea); + } + else + { + ViewModelRoutedViewHostMixins.ResultNavigating[Name].OnNext(ea); + } + } + + CanNavigateBack = NavigationStack.Count > 1; + _canNavigateBackSubject.OnNext(CanNavigateBack); + } + + /// + /// Refreshes this instance. + /// + public void Refresh() + { + // Keep existing view + if (CurrentPage == null && _currentView != null) + { + GotoPage(); + } + + if (!NavigateBackIsEnabled) + { + // cleanup while Navigation Back is disabled + while (NavigationStack.Count > 1) + { + NavigationStack.RemoveAt(0); + } + } + } + + /// + /// Setups this instance. + /// + /// NavigationShell Name not set. + public void Setup() + { + if (string.IsNullOrWhiteSpace(Name)) + { + throw new ArgumentNullException(Name, "NavigationShell Name not set"); + } + + _coreRegistration = ServiceLocator.Current().GetService(); + + var navigatingEvent = Observable.FromEvent, ShellNavigatingEventArgs>( + eventHandler => + { + void Handler(object? sender, ShellNavigatingEventArgs e) => eventHandler(e); + return Handler; + }, + x => Navigating += x, + x => Navigating -= x); + + var navigatedEvent = Observable.FromEvent, ShellNavigatedEventArgs>( + eventHandler => + { + void Handler(object? sender, ShellNavigatedEventArgs e) => eventHandler(e); + return Handler; + }, + x => Navigated += x, + x => Navigated -= x); + + navigatingEvent.Subscribe(e => + { + if ((e.Source == ShellNavigationSource.Pop || e.Source == ShellNavigationSource.PopToRoot) && !CanNavigateBack) + { + // Cancel navigate back + e.Cancel(); + } + + CanNavigateBack = NavigationStack?.Count > 1; + _canNavigateBackSubject.OnNext(CanNavigateBack); + }); + + navigatedEvent + .Subscribe(e => + { + var navigatingForward = false; + try + { + var f = e.Current.Location.OriginalString; + Debug.WriteLine($"Current {f}"); + if (e.Previous != null) + { + var s = e.Previous.Location.OriginalString; + Debug.WriteLine($"Previous {s}"); + } + + if ((e.Source == ShellNavigationSource.Pop || e.Source == ShellNavigationSource.PopToRoot) && NavigationStack.Count > 1) + { + // Navigating back + if (!_userInstigated) + { + if (NavigationStack.Count > 1) + { + NavigationStack.RemoveAt(NavigationStack.Count - 1); + } + + CanNavigateBack = NavigationStack?.Count > 1; + _canNavigateBackSubject.OnNext(CanNavigateBack); + } + } + + if (!_userInstigated && (e.Source == ShellNavigationSource.Push || e.Source == ShellNavigationSource.Insert || e.Source == ShellNavigationSource.ShellItemChanged || e.Source == ShellNavigationSource.ShellSectionChanged)) + { + // navigating forward + navigatingForward = true; + } + + if (CurrentPage is IAmViewFor page && !_userInstigated) + { + // don't replace view model if vm is null + var vm = __currentViewModel; + if (vm != null) + { + page.ViewModel ??= vm; + } + + if (navigatingForward && page.ViewModel is IRxNavBase pvm) + { + NavigationStack?.Add(pvm); + } + } + } + finally + { + _userInstigated = false; + } + }); + + // requested should return result here + ViewModelRoutedViewHostMixins.ResultNavigating[Name].DistinctUntilChanged() + .ObserveOn(_coreRegistration.MainThreadScheduler) + .Subscribe(e => + { + var fromView = _currentView as INotifiyNavigation; + if (fromView?.ISetupNavigating == false || fromView?.ISetupNavigating == null) + { + // No view is setup for recieving navigation notifications. + __currentViewModel?.WhenNavigating(e); + } + + if (!e.Cancel) + { + var nea = new ViewModelNavigationEventArgs(__currentViewModel, _toViewModel, _navigateBack ? NavigationType.Back : NavigationType.New, e.View, Name, e.NavigationParameter); + var toView = e.View as INotifiyNavigation; + var callVmNavTo = toView == null || !toView!.ISetupNavigatedTo; + var callVmNavFrom = fromView == null || !fromView!.ISetupNavigatedTo; + var cvm = __currentViewModel; + _toViewModel ??= e.View?.ViewModel as IRxNavBase; + var tvm = _toViewModel; + + if (_navigateBack) + { + if (tvm != null) + { + _currentView = ServiceLocator.Current().GetView(tvm); + _currentViewModel.OnNext(tvm); + foreach (var host in ViewModelRoutedViewHostMixins.NavigationHost.Where(x => x.Key != Name).Select(x => x.Key)) + { + ViewModelRoutedViewHostMixins.NavigationHost[host].Refresh(); + } + } + } + else if (tvm != null && _resetStack) + { + NavigationStack.Clear(); + _currentViewModel.OnNext(tvm); + } + else if (tvm != null && _currentView != null) + { + _currentViewModel.OnNext(tvm); + } + + if (toView?.ISetupNavigatedTo == true || fromView?.ISetupNavigatedFrom == true) + { + ViewModelRoutedViewHostMixins.SetWhenNavigated.OnNext(nea); + } + + if (callVmNavTo) + { + tvm?.WhenNavigatedTo(nea, ViewModelRoutedViewHostMixins.CurrentViewDisposable[Name]); + } + + if (callVmNavFrom) + { + cvm?.WhenNavigatedFrom(nea); + } + } + + CanNavigateBack = NavigationStack?.Count > 1; + _canNavigateBackSubject.OnNext(CanNavigateBack); + _resetStack = false; + }); + + OnAppearing(); + } + + /// + /// Converts to page. + /// + /// The item. + /// A Page. + protected static Page? ToPage(object item) => item as Page; + + private static void NameChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is NavigationShell ns) + { + ns.SetMainNavigationHost(ns); + } + } + + private async void GotoPage() + { + var page = ToPage(_currentView!); + + if (NavigationStack.Count == 1) + { + await Navigation.PopToRootAsync(true); + } + else if (_navigateBack) + { + await Navigation.PopAsync(true); + } + else + { + await Navigation.PushAsync(page, true); + } + + if (CurrentPage is IAmViewFor p && __currentViewModel is not null) + { + // don't replace view model if vm is null + p.ViewModel = __currentViewModel; + } + } + + private void InternalNavigate(string? contract, object? parameter) + where T : class, IRxNavBase + { + _userInstigated = true; + _toViewModel = ServiceLocator.Current().GetServiceWithContract(contract); + _lastView = _currentView; + + // NOTE: This gets a new instance of the View + _currentView = ServiceLocator.Current().GetView(contract); + + var ea = new ViewModelNavigatingEventArgs(__currentViewModel, _toViewModel, NavigationType.New, _currentView, Name, parameter); + if (_currentView is INotifiyNavigation { ISetupNavigating: true }) + { + ViewModelRoutedViewHostMixins.SetWhenNavigating.OnNext(ea); + } + else + { + ViewModelRoutedViewHostMixins.ResultNavigating[Name].OnNext(ea); + } + } + + private void InternalNavigate(IRxNavBase viewModel, object? parameter) + { + _userInstigated = true; + _toViewModel = viewModel; + _lastView = _currentView; + + // NOTE: This gets a new instance of the View + _currentView = ServiceLocator.Current().GetView(viewModel); + + var ea = new ViewModelNavigatingEventArgs(__currentViewModel, _toViewModel, NavigationType.New, _currentView, Name, parameter); + if (_currentView is INotifiyNavigation { ISetupNavigating: true }) + { + ViewModelRoutedViewHostMixins.SetWhenNavigating.OnNext(ea); + } + else + { + ViewModelRoutedViewHostMixins.ResultNavigating[Name].OnNext(ea); + } + } +} diff --git a/src/ReactiveMarbles.ViewModel.MAUI/ReactiveMarbles.ViewModel.MAUI.csproj b/src/ReactiveMarbles.ViewModel.MAUI/ReactiveMarbles.ViewModel.MAUI.csproj new file mode 100644 index 0000000..e0efeaf --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/ReactiveMarbles.ViewModel.MAUI.csproj @@ -0,0 +1,28 @@ + + + + net6.0-android;net6.0-ios;net6.0-maccatalyst;net7.0-android;net7.0-ios;net7.0-maccatalyst + $(TargetFrameworks);net6.0-windows10.0.19041.0;net7.0-windows10.0.19041.0 + enable + enable + true + false + + + WINUI + + + MAUIANDROID + + + MAUIMAC + + + MAUIMAC + + + + + + + diff --git a/src/ReactiveMarbles.ViewModel.MAUI/RxContentPage{TViewModel}.cs b/src/ReactiveMarbles.ViewModel.MAUI/RxContentPage{TViewModel}.cs new file mode 100644 index 0000000..dec3927 --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/RxContentPage{TViewModel}.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2019-2023 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 ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.ViewModel.MAUI; + +/// +/// This is an that is also an . +/// +/// The type of the view model. +/// +public class RxContentPage : ContentPage, IAmViewFor + where TViewModel : class, IRxNavBase +{ + /// + /// The view model bindable property. + /// + public static readonly BindableProperty ViewModelProperty = BindableProperty.Create( + nameof(ViewModel), + typeof(TViewModel), + typeof(RxContentPage), + default(TViewModel), + BindingMode.OneWay, + propertyChanged: OnViewModelChanged); + + /// + /// Gets or sets the ViewModel to display. + /// + public TViewModel? ViewModel + { + get => (TViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + object? IAmViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + ViewModel = BindingContext as TViewModel; + } + + private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue) => + bindableObject.BindingContext = newValue; +} diff --git a/src/ReactiveMarbles.ViewModel.MAUI/RxShellContent{TViewModel}.cs b/src/ReactiveMarbles.ViewModel.MAUI/RxShellContent{TViewModel}.cs new file mode 100644 index 0000000..4e3d239 --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/RxShellContent{TViewModel}.cs @@ -0,0 +1,88 @@ +// Copyright (c) 2019-2023 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 ReactiveMarbles.Locator; +using ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.ViewModel.MAUI; + +/// +/// ReactiveShellContent. +/// +/// The type of the view model. +/// +public class RxShellContent : ShellContent + where TViewModel : class, IRxNavBase +{ + /// + /// The contract property. + /// + public static readonly BindableProperty ContractProperty = BindableProperty.Create( + nameof(Contract), + typeof(string), + typeof(RxShellContent), + null, + BindingMode.Default, + propertyChanged: ViewModelChanged); + + /// + /// The view model property. + /// + public static readonly BindableProperty ViewModelProperty = BindableProperty.Create( + nameof(ViewModel), + typeof(TViewModel), + typeof(RxShellContent), + default(TViewModel), + BindingMode.Default, + propertyChanged: ViewModelChanged); + + /// + /// Initializes a new instance of the class. + /// + public RxShellContent() + { + var view = ServiceLocator.Current().GetServiceWithContract>(Contract); + if (view is not null) + { + ContentTemplate = new DataTemplate(() => view); + } + } + + /// + /// Gets or sets the view model. + /// + /// + /// The view model. + /// + public TViewModel? ViewModel + { + get => (TViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + /// Gets or sets the contract for the view. + /// + /// + /// The contract. + /// + public string? Contract + { + get => (string?)GetValue(ContractProperty); + set => SetValue(ContractProperty, value); + } + + private static void ViewModelChanged(BindableObject bindable, object oldValue, object newValue) + { + if (bindable is RxShellContent svm) + { + var view = ServiceLocator.Current().GetServiceWithContract>(svm.Contract); + + if (view is not null) + { + svm.ContentTemplate = new DataTemplate(() => view); + } + } + } +} diff --git a/src/ReactiveMarbles.ViewModel.MAUI/RxShell{TViewModel}.cs b/src/ReactiveMarbles.ViewModel.MAUI/RxShell{TViewModel}.cs new file mode 100644 index 0000000..0a44c87 --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/RxShell{TViewModel}.cs @@ -0,0 +1,52 @@ +// Copyright (c) 2019-2023 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 ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.ViewModel.MAUI; + +/// +/// ReactiveShell. +/// +/// The type of the view model. +/// +public class RxShell : Shell, IAmViewFor + where TViewModel : class, IRxNavBase +{ + /// + /// The view model bindable property. + /// + public static readonly BindableProperty ViewModelProperty = BindableProperty.Create( + nameof(ViewModel), + typeof(TViewModel), + typeof(RxShell), + default(TViewModel), + BindingMode.OneWay, + propertyChanged: OnViewModelChanged); + + /// + /// Gets or sets the ViewModel to display. + /// + public TViewModel? ViewModel + { + get => (TViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + /// + object? IAmViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (TViewModel?)value; + } + + /// + protected override void OnBindingContextChanged() + { + base.OnBindingContextChanged(); + ViewModel = BindingContext as TViewModel; + } + + private static void OnViewModelChanged(BindableObject bindableObject, object oldValue, object newValue) => bindableObject.BindingContext = newValue; +} diff --git a/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiAndroidScheduler.cs b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiAndroidScheduler.cs new file mode 100644 index 0000000..15ab064 --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiAndroidScheduler.cs @@ -0,0 +1,99 @@ +// Copyright (c) 2019-2023 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. + +#if MAUIANDROID +using System; +using System.Reactive.Disposables; +using Android.OS; + +namespace System.Reactive.Concurrency; + +/// +/// MauiAndroidScheduler is a scheduler that schedules items on a running +/// Activity's main thread. This is the moral equivalent of +/// DispatcherScheduler. +/// +public class MauiAndroidScheduler : IScheduler +{ + private readonly Handler _handler; + private readonly long _looperId; + + static MauiAndroidScheduler() => + MainThreadScheduler = new MauiAndroidScheduler(new Handler(Looper.MainLooper!), Looper.MainLooper?.Thread?.Id); + + /// + /// Initializes a new instance of the class. + /// + /// The handler. + /// The thread identifier associated with handler. + public MauiAndroidScheduler(Handler handler, long? threadIdAssociatedWithHandler) + { + _handler = handler; + _looperId = threadIdAssociatedWithHandler ?? -1; + } + + /// + /// Gets a common instance to avoid allocations to the MainThread for the MauiAndroidScheduler. + /// + public static IScheduler MainThreadScheduler { get; } + + /// + public DateTimeOffset Now => DateTimeOffset.Now; + + /// + public IDisposable Schedule(TState state, Func action) + { + var isCancelled = false; + var innerDisp = new SerialDisposable() { Disposable = Disposable.Empty }; + + _handler.Post(() => + { + if (isCancelled) + { + return; + } + + innerDisp.Disposable = action(this, state); + }); + + return new CompositeDisposable( + Disposable.Create(() => isCancelled = true), + innerDisp); + } + + /// + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) // TODO: Create Test + { + var isCancelled = false; + var innerDisp = new SerialDisposable() { Disposable = Disposable.Empty }; + + _handler.PostDelayed( + () => + { + if (isCancelled) + { + return; + } + + innerDisp.Disposable = action(this, state); + }, + dueTime.Ticks / 10 / 1000); + + return new CompositeDisposable( + Disposable.Create(() => isCancelled = true), + innerDisp); + } + + /// + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) // TODO: Create Test + { + if (dueTime <= Now) + { + return Schedule(state, action); + } + + return Schedule(state, dueTime - Now, action); + } +} +#endif diff --git a/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiDispatcherScheduler.cs b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiDispatcherScheduler.cs new file mode 100644 index 0000000..75c1d3e --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiDispatcherScheduler.cs @@ -0,0 +1,64 @@ +// Copyright (c) 2019-2023 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. + +#if WINUI || MAUIMAC + +namespace System.Reactive.Concurrency; + +/// +/// MauiWinUIDispatcherScheduler. +/// +public class MauiDispatcherScheduler : IScheduler +{ + private readonly Func _schedulerFactory; + private IScheduler? _scheduler; + + /// + /// Initializes a new instance of the class. + /// + /// A func which will return a new scheduler. + public MauiDispatcherScheduler(Func schedulerFactory) + { + _schedulerFactory = schedulerFactory; + AttemptToCreateScheduler(); + } + + /// + public DateTimeOffset Now => AttemptToCreateScheduler().Now; + + /// + public IDisposable Schedule(TState state, Func action) => + AttemptToCreateScheduler().Schedule(state, action); + + /// + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) => // TODO: Create Test + AttemptToCreateScheduler().Schedule(state, dueTime, action); + + /// + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) => // TODO: Create Test + AttemptToCreateScheduler().Schedule(state, dueTime, action); + + private IScheduler AttemptToCreateScheduler() + { + if (_scheduler is not null) + { + return _scheduler; + } + + try + { + _scheduler = _schedulerFactory(); + return _scheduler; + } + catch (InvalidOperationException) + { + return CurrentThreadScheduler.Instance; + } + catch (ArgumentNullException) + { + return CurrentThreadScheduler.Instance; + } + } +} +#endif diff --git a/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiMacScheduler.cs b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiMacScheduler.cs new file mode 100644 index 0000000..00d2fff --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiMacScheduler.cs @@ -0,0 +1,72 @@ +// Copyright (c) 2019-2023 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. + +#if MAUIMAC + +using System; +using System.Reactive.Disposables; +using CoreFoundation; +using Foundation; +using NSAction = System.Action; + +namespace System.Reactive.Concurrency; + +/// +/// MauiMacScheduler. +/// +public class MauiMacScheduler : IScheduler +{ + /// + public DateTimeOffset Now => DateTimeOffset.Now; + + /// + public IDisposable Schedule(TState state, Func action) + { + var innerDisp = new SingleAssignmentDisposable(); + + DispatchQueue.MainQueue.DispatchAsync(new NSAction(() => + { + if (!innerDisp.IsDisposed) + { + innerDisp.Disposable = action(this, state); + } + })); + + return innerDisp; + } + + /// + public IDisposable Schedule(TState state, DateTimeOffset dueTime, Func action) + { + if (dueTime <= Now) + { + return Schedule(state, action); + } + + return Schedule(state, dueTime - Now, action); + } + + /// + public IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + var innerDisp = Disposable.Empty; + var isCancelled = false; + + var timer = NSTimer.CreateScheduledTimer(dueTime, _ => + { + if (!isCancelled) + { + innerDisp = action(this, state); + } + }); + + return Disposable.Create(() => + { + isCancelled = true; + timer.Invalidate(); + innerDisp.Dispose(); + }); + } +} +#endif diff --git a/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiWinUIScheduler.cs b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiWinUIScheduler.cs new file mode 100644 index 0000000..d466a8e --- /dev/null +++ b/src/ReactiveMarbles.ViewModel.MAUI/Schedulers/MauiWinUIScheduler.cs @@ -0,0 +1,201 @@ +// Copyright (c) 2019-2023 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. + +#if WINUI +using System.Reactive.Disposables; +using Microsoft.UI.Dispatching; + +namespace System.Reactive.Concurrency; + +/// +/// MauiWinUIScheduler. +/// +/// +/// +public class MauiWinUIScheduler : LocalScheduler, ISchedulerPeriodic +{ + /// + /// Initializes a new instance of the class. + /// Constructs a that schedules units of work on the given . + /// + /// to schedule work on. + /// is null. + public MauiWinUIScheduler(DispatcherQueue dispatcherQueue) + { + DispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue)); + Priority = DispatcherQueuePriority.Normal; + } + + /// + /// Initializes a new instance of the class. + /// Constructs a DispatcherScheduler that schedules units of work on the given at the given priority. + /// + /// to schedule work on. + /// Priority at which units of work are scheduled. + /// is null. + public MauiWinUIScheduler(DispatcherQueue dispatcherQueue, DispatcherQueuePriority priority) + { + DispatcherQueue = dispatcherQueue ?? throw new ArgumentNullException(nameof(dispatcherQueue)); + Priority = priority; + } + + /// + /// Gets the scheduler that schedules work on the for the current thread. + /// + public static MauiWinUIScheduler Current + { + get + { + var dispatcher = DispatcherQueue.GetForCurrentThread() ?? throw new InvalidOperationException("There is no current dispatcher thread"); + return new MauiWinUIScheduler(dispatcher); + } + } + + /// + /// Gets the />. + /// + public DispatcherQueue DispatcherQueue { get; } + + /// + /// Gets the priority at which work items will be dispatched. + /// + public DispatcherQueuePriority Priority { get; } + + /// + /// Schedules an action to be executed on the dispatcher. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + /// is null. + public override IDisposable Schedule(TState state, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var d = new SingleAssignmentDisposable(); + + DispatcherQueue.TryEnqueue( + Priority, + () => + { + if (!d.IsDisposed) + { + d.Disposable = action(this, state); + } + }); + + return d; + } + + /// + /// Schedules an action to be executed after on the dispatcherQueue, using a object. + /// + /// The type of the state passed to the scheduled action. + /// State passed to the action to be executed. + /// Relative time after which to execute the action. + /// Action to be executed. + /// The disposable object used to cancel the scheduled action (best effort). + /// is null. + public override IDisposable Schedule(TState state, TimeSpan dueTime, Func action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var dt = Scheduler.Normalize(dueTime); + if (dt.Ticks == 0) + { + return Schedule(state, action); + } + + return ScheduleSlow(state, dt, action); + } + + /// + /// Schedules a periodic piece of work on the dispatcherQueue, using a object. + /// + /// The type of the state passed to the scheduled action. + /// Initial state passed to the action upon the first iteration. + /// Period for running the work periodically. + /// Action to be executed, potentially updating the state. + /// The disposable object used to cancel the scheduled recurring action (best effort). + /// is null. + /// is less than . + public IDisposable SchedulePeriodic(TState state, TimeSpan period, Func action) + { + if (period < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(period)); + } + + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + var timer = DispatcherQueue.CreateTimer(); + + var state1 = state; + + timer.Tick += (_, __) => state1 = action(state1); + + timer.Interval = period; + timer.Start(); + + return Disposable.Create(() => + { + var t = Interlocked.Exchange(ref timer, null); + if (t != null) + { + t.Stop(); + action = static _ => _; + } + }); + } + + private IDisposable ScheduleSlow(TState state, TimeSpan dueTime, Func action) + { + var d = new MultipleAssignmentDisposable(); + + var timer = DispatcherQueue.CreateTimer(); + + timer.Tick += (s, e) => + { + var t = Interlocked.Exchange(ref timer, null); + if (t != null) + { + try + { + d.Disposable = action(this, state); + } + finally + { + t.Stop(); + action = static (s, t) => Disposable.Empty; + } + } + }; + + timer.Interval = dueTime; + timer.Start(); + + d.Disposable = Disposable.Create(() => + { + var t = Interlocked.Exchange(ref timer, null); + if (t != null) + { + t.Stop(); + action = static (s, t) => Disposable.Empty; + } + }); + + return d; + } +} +#endif diff --git a/src/ViewModel.Core/IAmViewFor.cs b/src/ViewModel.Core/IAmViewFor.cs new file mode 100644 index 0000000..8446542 --- /dev/null +++ b/src/ViewModel.Core/IAmViewFor.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// IAmViewFor. +/// +public interface IAmViewFor +{ + /// + /// Gets or sets the View Model associated with the View. + /// + object? ViewModel { get; set; } +} diff --git a/src/ViewModel.Core/IAmViewFor{T}.cs b/src/ViewModel.Core/IAmViewFor{T}.cs new file mode 100644 index 0000000..c759052 --- /dev/null +++ b/src/ViewModel.Core/IAmViewFor{T}.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// IAmViewFor. +/// +/// The type of ViewModel. +public interface IAmViewFor : IAmViewFor +where T : class +{ + /// + /// Gets or sets the ViewModel corresponding to this specific View. This should be + /// a DependencyProperty if you're using XAML. + /// + new T? ViewModel { get; set; } +} diff --git a/src/ViewModel.Core/INotifiyRoutableViewModel.cs b/src/ViewModel.Core/INotifiyRoutableViewModel.cs new file mode 100644 index 0000000..0a40572 --- /dev/null +++ b/src/ViewModel.Core/INotifiyRoutableViewModel.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2019-2023 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.Disposables; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// INotifiy Routable ViewModel. +/// +/// +public interface INotifiyRoutableViewModel : Mvvm.IRxObject, IUseHostedNavigation +{ + /// + /// Gets the name. + /// + /// + /// The name. + /// + string? Name { get; } + + /// + /// Raises the event. + /// + /// + /// The instance containing the event data. + /// + void WhenNavigatedFrom(IViewModelNavigationEventArgs e); + + /// + /// Raises the event. + /// + /// + /// The instance containing the event data. + /// + /// The disposables. + void WhenNavigatedTo(IViewModelNavigationEventArgs e, CompositeDisposable disposables); + + /// + /// Raises the event. + /// + /// + /// The instance containing the event data. + /// + void WhenNavigating(IViewModelNavigatingEventArgs e); +} diff --git a/src/ViewModel.Core/IRxNavBase.cs b/src/ViewModel.Core/IRxNavBase.cs new file mode 100644 index 0000000..7b54971 --- /dev/null +++ b/src/ViewModel.Core/IRxNavBase.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2019-2023 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.Disposables; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// interface for RxBase. +/// +/// +public interface IRxNavBase : INotifiyRoutableViewModel, ICancelable, IAmBuilt +{ +} diff --git a/src/ViewModel.Core/IViewModelRoutedViewHost.cs b/src/ViewModel.Core/IViewModelRoutedViewHost.cs new file mode 100644 index 0000000..c581342 --- /dev/null +++ b/src/ViewModel.Core/IViewModelRoutedViewHost.cs @@ -0,0 +1,122 @@ +// Copyright (c) 2019-2023 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.Collections.ObjectModel; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// IViewModel Routed ViewHost. +/// +public interface IViewModelRoutedViewHost +{ + /// + /// Gets the navigation stack. + /// + /// + /// The navigation stack. + /// + ObservableCollection NavigationStack { get; } + + /// + /// Gets the current view model. + /// + /// + /// The current view model. + /// + IObservable CurrentViewModel { get; } + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// + /// true if [navigate back is enabled]; otherwise, false. + /// + bool CanNavigateBack { get; set; } + + /// + /// Gets the can navigate back observable. + /// + /// + /// The can navigate back observable. + /// + IObservable CanNavigateBackObservable { get; } + + /// + /// Gets or sets a value indicating whether [navigate back is enabled]. + /// + /// + /// true if [navigate back is enabled]; otherwise, false. + /// + bool NavigateBackIsEnabled { get; set; } + + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + string Name { get; set; } + + /// + /// Gets a value indicating whether [requires setup]. + /// + /// + /// true if [requires setup]; otherwise, false. + /// + bool RequiresSetup { get; } + + /// + /// Clears the history. + /// + void ClearHistory(); + + /// + /// Setups this instance. + /// + void Setup(); + + /// + /// Navigates the specified contract. + /// + /// The Type. + /// The contract. + /// The parameter. + void Navigate(string? contract = null, object? parameter = null) + where T : class, IRxNavBase; + + /// + /// Navigates the specified contract. + /// + /// The view model. + /// The parameter. + void Navigate(IRxNavBase viewModel, object? parameter = null); + + /// + /// Navigates the and reset. + /// + /// The Type. + /// The contract. + /// The parameter. + void NavigateAndReset(string? contract = null, object? parameter = null) + where T : class, IRxNavBase; + + /// + /// Navigates the and reset. + /// + /// The view model. + /// The parameter. + void NavigateAndReset(IRxNavBase viewModel, object? parameter = null); + + /// + /// Navigates the back. + /// + /// The parameter. + void NavigateBack(object? parameter = null); + + /// + /// Refreshes this instance. + /// + void Refresh(); +} diff --git a/src/ViewModel.Core/MagicInterfaces/IAmBuilt.cs b/src/ViewModel.Core/MagicInterfaces/IAmBuilt.cs new file mode 100644 index 0000000..f579c82 --- /dev/null +++ b/src/ViewModel.Core/MagicInterfaces/IAmBuilt.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// IAmBuilt. +/// +public interface IAmBuilt +{ +} diff --git a/src/ViewModel.Core/MagicInterfaces/INotifiyNavigation.cs b/src/ViewModel.Core/MagicInterfaces/INotifiyNavigation.cs new file mode 100644 index 0000000..e9aa926 --- /dev/null +++ b/src/ViewModel.Core/MagicInterfaces/INotifiyNavigation.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2019-2023 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.Disposables; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// INotifiy Navigation. +/// +/// +public interface INotifiyNavigation : ICancelable +{ + /// + /// Gets or sets a value indicating whether [i setup navigated to]. + /// + /// + /// true if [i setup navigated to]; otherwise, false. + /// + bool ISetupNavigatedTo { get; set; } + + /// + /// Gets or sets a value indicating whether [i setup navigated from]. + /// + /// + /// true if [i setup navigated from]; otherwise, false. + /// + bool ISetupNavigatedFrom { get; set; } + + /// + /// Gets or sets a value indicating whether [i setup navigating]. + /// + /// + /// true if [i setup navigating]; otherwise, false. + /// + bool ISetupNavigating { get; set; } + + /// + /// Gets the clean up. + /// + /// + /// The clean up. + /// + CompositeDisposable CleanUp { get; } +} diff --git a/src/ViewModel.Core/MagicInterfaces/ISetNavigation.cs b/src/ViewModel.Core/MagicInterfaces/ISetNavigation.cs new file mode 100644 index 0000000..8505e8a --- /dev/null +++ b/src/ViewModel.Core/MagicInterfaces/ISetNavigation.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// Enables setting of ViewModelRoutedViewHost. +/// +public interface ISetNavigation +{ + /// + /// Gets the name. + /// + /// + /// The name. + /// + string Name { get; } +} diff --git a/src/ViewModel.Core/MagicInterfaces/IUseHostedNavigation.cs b/src/ViewModel.Core/MagicInterfaces/IUseHostedNavigation.cs new file mode 100644 index 0000000..b84c762 --- /dev/null +++ b/src/ViewModel.Core/MagicInterfaces/IUseHostedNavigation.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// Enables Navigation commands specified by host name. +/// +public interface IUseHostedNavigation +{ +} diff --git a/src/ViewModel.Core/MagicInterfaces/IUseNavigation.cs b/src/ViewModel.Core/MagicInterfaces/IUseNavigation.cs new file mode 100644 index 0000000..52adf2e --- /dev/null +++ b/src/ViewModel.Core/MagicInterfaces/IUseNavigation.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// I Use Navigation. +/// +public interface IUseNavigation +{ + /// + /// Gets the name. + /// + /// + /// The name. + /// + string Name { get; } +} diff --git a/src/ViewModel.Core/NavigationEvents/IViewModelNavigatingEventArgs.cs b/src/ViewModel.Core/NavigationEvents/IViewModelNavigatingEventArgs.cs new file mode 100644 index 0000000..06972b4 --- /dev/null +++ b/src/ViewModel.Core/NavigationEvents/IViewModelNavigatingEventArgs.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// IView Model Navigating EventArgs. +/// +public interface IViewModelNavigatingEventArgs : IViewModelNavigationEventArgs +{ + /// + /// Gets or sets a value indicating whether this is cancel. + /// + /// + /// true if cancel; otherwise, false. + /// + bool Cancel { get; set; } +} diff --git a/src/ViewModel.Core/NavigationEvents/IViewModelNavigationBaseEventArgs.cs b/src/ViewModel.Core/NavigationEvents/IViewModelNavigationBaseEventArgs.cs new file mode 100644 index 0000000..f251b44 --- /dev/null +++ b/src/ViewModel.Core/NavigationEvents/IViewModelNavigationBaseEventArgs.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// IView Model Navigation Base Event Args. +/// +public interface IViewModelNavigationBaseEventArgs +{ + /// + /// Gets from. + /// + /// + /// From. + /// + IRxNavBase? From { get; } + + /// + /// Gets the navigation parameter. + /// + /// + /// The navigation parameter. + /// + object? NavigationParameter { get; } + + /// + /// Gets to. + /// + /// + /// To. + /// + IRxNavBase? To { get; } +} diff --git a/src/ViewModel.Core/NavigationEvents/IViewModelNavigationEventArgs.cs b/src/ViewModel.Core/NavigationEvents/IViewModelNavigationEventArgs.cs new file mode 100644 index 0000000..a3a87fe --- /dev/null +++ b/src/ViewModel.Core/NavigationEvents/IViewModelNavigationEventArgs.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// I View Model Navigation EventArgs. +/// +public interface IViewModelNavigationEventArgs : IViewModelNavigationBaseEventArgs +{ + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + string HostName { get; set; } + + /// + /// Gets the type of the navigation. + /// + /// + /// The type of the navigation. + /// + NavigationType NavigationType { get; } + + /// + /// Gets or sets the view. + /// + /// + /// The view. + /// + IAmViewFor? View { get; set; } +} diff --git a/src/ViewModel.Core/NavigationEvents/ViewModelNavigatingEventArgs.cs b/src/ViewModel.Core/NavigationEvents/ViewModelNavigatingEventArgs.cs new file mode 100644 index 0000000..2969923 --- /dev/null +++ b/src/ViewModel.Core/NavigationEvents/ViewModelNavigatingEventArgs.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2019-2023 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.Runtime.Serialization; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// View Model Navigating Event Args. +/// +[DataContract] +public class ViewModelNavigatingEventArgs : ViewModelNavigationEventArgs, IViewModelNavigatingEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// From. + /// To. + /// Type of the nav. + /// The view. + /// The hostName. + /// The parmeter. + public ViewModelNavigatingEventArgs(IRxNavBase? from, IRxNavBase? to, NavigationType navType, IAmViewFor? view, string hostName, object? parmeter = null) + : base(from, to, navType, view, hostName, parmeter) + { + } + + /// + /// Gets or sets a value indicating whether this + /// is canceled. + /// + /// true if cancel; otherwise, false. + [DataMember] + public bool Cancel { get; set; } +} diff --git a/src/ViewModel.Core/NavigationEvents/ViewModelNavigationBaseEventArgs.cs b/src/ViewModel.Core/NavigationEvents/ViewModelNavigationBaseEventArgs.cs new file mode 100644 index 0000000..561f919 --- /dev/null +++ b/src/ViewModel.Core/NavigationEvents/ViewModelNavigationBaseEventArgs.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2019-2023 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.Runtime.Serialization; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// View Model Navigation Base Event Args. +/// +/// +[DataContract] +public abstract class ViewModelNavigationBaseEventArgs + : EventArgs, IViewModelNavigationBaseEventArgs +{ + /// + /// Gets or sets where is Navigating from. + /// + /// From. + [DataMember] + public IRxNavBase? From { get; protected set; } + + /// + /// Gets or sets the navigation parameter. + /// + /// The navigation parameter. + [DataMember] + public object? NavigationParameter { get; protected set; } + + /// + /// Gets or sets where is Navigating to. + /// + /// To. + [DataMember] + public IRxNavBase? To { get; protected set; } +} diff --git a/src/ViewModel.Core/NavigationEvents/ViewModelNavigationEventArgs.cs b/src/ViewModel.Core/NavigationEvents/ViewModelNavigationEventArgs.cs new file mode 100644 index 0000000..142115f --- /dev/null +++ b/src/ViewModel.Core/NavigationEvents/ViewModelNavigationEventArgs.cs @@ -0,0 +1,54 @@ +// Copyright (c) 2019-2023 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.Runtime.Serialization; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// View Model Navigation EventArgs. +/// +[DataContract] +public class ViewModelNavigationEventArgs : ViewModelNavigationBaseEventArgs, IViewModelNavigationEventArgs +{ + /// Initializes a new instance of the class. + /// From. + /// To. + /// Type of the nav. + /// The view. + /// The Hostname. + /// The parmeter. + public ViewModelNavigationEventArgs(IRxNavBase? from, IRxNavBase? to, NavigationType navType, IAmViewFor? view, string hostName, object? parmeter = null) + { + From = from; + To = to; + View = view; + NavigationType = navType; + NavigationParameter = parmeter; + HostName = hostName; + } + + /// + /// Gets or sets the name of the host. + /// + /// + /// The name of the host. + /// + [DataMember] + public string HostName { get; set; } + + /// + /// Gets or sets the type of the navigation. + /// + /// The type of the navigation. + [DataMember] + public NavigationType NavigationType { get; protected set; } + + /// + /// Gets or sets the view. + /// + /// The view. + [DataMember] + public IAmViewFor? View { get; set; } +} diff --git a/src/ViewModel.Core/NavigationType.cs b/src/ViewModel.Core/NavigationType.cs new file mode 100644 index 0000000..5841ac3 --- /dev/null +++ b/src/ViewModel.Core/NavigationType.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2019-2023 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. + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// Identifies the types of navigation that are supported. +/// +public enum NavigationType +{ + /// + /// Navigating to new content. + /// + New = 0, + + /// + /// Navigating back in the back navigation history. + /// + Back = 1, + + /// + /// Reloading the current content. + /// + Refresh = 2 +} diff --git a/src/ViewModel.Core/ReactiveMarbles.ViewModel.Core.csproj b/src/ViewModel.Core/ReactiveMarbles.ViewModel.Core.csproj new file mode 100644 index 0000000..d2ea1ac --- /dev/null +++ b/src/ViewModel.Core/ReactiveMarbles.ViewModel.Core.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0;net6.0;net6.0-android;net6.0-ios;net6.0-tvos;net6.0-macos;net7.0;net7.0-android;net7.0-ios;net7.0-tvos;net7.0-macos + $(TargetFrameworks);net462;net472;net6.0-windows10.0.19041.0;net7.0-windows10.0.19041.0 + enable + false + latest + enable + + + + + + + diff --git a/src/ViewModel.Core/RxNavBase.cs b/src/ViewModel.Core/RxNavBase.cs new file mode 100644 index 0000000..9ec8e9f --- /dev/null +++ b/src/ViewModel.Core/RxNavBase.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2019-2023 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.Disposables; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// Rx Object. +/// +/// +/// +public abstract class RxNavBase : Mvvm.RxObject, IRxNavBase +{ + /// + /// Initializes a new instance of the class. + /// + protected RxNavBase() + { + } + + /// + /// Gets the URL path segment. + /// + /// + /// The URL path segment. + /// + public string? Name => GetType().FullName; + + /// + /// Gets a value indicating whether this instance is disposed. + /// + /// true if this instance is disposed; otherwise, false. + public bool IsDisposed => Disposables?.IsDisposed == true; + + /// + /// Gets the disposables. + /// + /// + /// The disposables. + /// + protected CompositeDisposable Disposables { get; } = new CompositeDisposable(); + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting + /// unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + public virtual void WhenNavigatedFrom(IViewModelNavigationEventArgs e) + { + } + + /// + public virtual void WhenNavigatedTo(IViewModelNavigationEventArgs e, CompositeDisposable disposables) + { + } + + /// + public virtual void WhenNavigating(IViewModelNavigatingEventArgs e) + { + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// + /// true to release both managed and unmanaged resources; false to release only + /// unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!IsDisposed && disposing) + { + Disposables?.Dispose(); + } + } +} diff --git a/src/ViewModel.Core/RxObjectMixins.cs b/src/ViewModel.Core/RxObjectMixins.cs new file mode 100644 index 0000000..f56e170 --- /dev/null +++ b/src/ViewModel.Core/RxObjectMixins.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2019-2023 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 System.Reactive.Linq; +using System.Reactive.Subjects; +using ReactiveMarbles.Locator; + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// RxObjectMixins. +/// +public static class RxObjectMixins +{ + private static readonly ReplaySubject _buildCompleteSubject = new(1); + + /// + /// Sets the IOC container build complete, Execute this once after completion of IOC registrations. + /// + /// The dummy. +#pragma warning disable RCS1175 // Unused 'this' parameter. + public static void SetupComplete(this IEditServices dummy) => _buildCompleteSubject.OnNext(Unit.Default); + + /// + /// Gets the build complete. + /// + /// The dummy. + /// The action. + /// The build complete. + public static void BuildComplete(this IAmBuilt dummy, Action action) => _buildCompleteSubject.Subscribe(_ => action()); +#pragma warning restore RCS1175 // Unused 'this' parameter. +} diff --git a/src/ViewModel.Core/ServiceLocatorMixins.cs b/src/ViewModel.Core/ServiceLocatorMixins.cs new file mode 100644 index 0000000..f8a8621 --- /dev/null +++ b/src/ViewModel.Core/ServiceLocatorMixins.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2019-2023 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 ReactiveMarbles.ViewModel.Core; + +namespace ReactiveMarbles.Locator; + +/// +/// ServiceLocatorMixins. +/// +public static class ServiceLocatorMixins +{ + /// + /// Adds the navigation view. + /// + /// The type of the view. + /// The type of the view model. + /// The service locator. + public static void AddNavigationView(this IServiceLocator serviceLocator) + where TView : class, IAmViewFor, new() + where TViewModel : class, IRxNavBase + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + serviceLocator.AddService(() => new TView(), typeof(TViewModel).FullName!); + serviceLocator.AddService>(() => new TView()); + serviceLocator.AddService(() => ServiceLocator.Current().GetService>() as TView); + } + + /// + /// Adds the navigation view. + /// + /// The type of the view. + /// The type of the view model. + /// The service locator. + /// The contract. + /// serviceLocator. + public static void AddNavigationView(this IServiceLocator serviceLocator, string contract) + where TView : class, IAmViewFor, new() + where TViewModel : class, IRxNavBase + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + serviceLocator.AddService(() => new TView(), typeof(TViewModel).FullName!); + serviceLocator.AddService>(() => new TView(), contract); + serviceLocator.AddService(() => ServiceLocator.Current().GetService>() as TView, contract); + } + + /// + /// Gets the service. + /// + /// The Type. + /// The service locator. + /// The contract. + /// + /// An instance. + /// + /// serviceLocator. + public static IAmViewFor? GetView(this IServiceLocator serviceLocator, string? contract = null) + where T : class, IRxNavBase + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + return serviceLocator.GetServiceWithContract>(contract); + } + + /// + /// Gets the service. + /// + /// The service locator. + /// The view model. + /// + /// An instance. + /// + /// serviceLocator. + public static IAmViewFor? GetView(this IServiceLocator serviceLocator, IRxNavBase viewModel) + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + if (viewModel == null) + { + throw new ArgumentNullException(nameof(viewModel)); + } + + return (IAmViewFor)serviceLocator.GetServiceWithContract(viewModel.GetType().FullName); + } + + /// + /// Gets the service. + /// + /// The type. + /// The service locator. + /// The contract. + /// An instance of T. + /// serviceLocator. + public static T GetServiceWithContract(this IServiceLocator serviceLocator, string? contract = null) + { + if (serviceLocator == null) + { + throw new ArgumentNullException(nameof(serviceLocator)); + } + + if (string.IsNullOrWhiteSpace(contract)) + { + return serviceLocator.GetService(); + } + + return serviceLocator.GetService(contract!); + } +} diff --git a/src/ViewModel.Core/ViewModelRoutedViewHostMixins.cs b/src/ViewModel.Core/ViewModelRoutedViewHostMixins.cs new file mode 100644 index 0000000..652abb3 --- /dev/null +++ b/src/ViewModel.Core/ViewModelRoutedViewHostMixins.cs @@ -0,0 +1,614 @@ +// Copyright (c) 2019-2023 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 System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Runtime.CompilerServices; +using ReactiveMarbles.Extensions; + +[assembly: InternalsVisibleTo("ReactiveMarbles.ViewModel.Wpf")] +[assembly: InternalsVisibleTo("ReactiveMarbles.ViewModel.WinForms")] +[assembly: InternalsVisibleTo("ReactiveMarbles.ViewModel.MAUI")] +[assembly: InternalsVisibleTo("ReactiveMarbles.ViewModel.Avalonia")] + +namespace ReactiveMarbles.ViewModel.Core; + +/// +/// View Model Routed View Host Mixins. +/// +public static class ViewModelRoutedViewHostMixins +{ +#pragma warning disable RCS1175 // Unused 'this' parameter. + + internal static ReplaySubject ASetupCompleted { get; } = new(1); + + internal static Dictionary CurrentViewDisposable { get; } = new(); + + internal static Dictionary NavigationHost { get; } = new(); + + internal static Dictionary> ResultNavigating { get; } = new(); + + internal static Subject SetWhenNavigated { get; } = new(); + + internal static Subject SetWhenNavigating { get; } = new(); + + internal static Dictionary> WhenSetupSubjects { get; } = new(); + + /// + /// Determines whether this instance [can navigate back] the specified this. + /// + /// The this. + /// + /// A bool. + /// + /// this. + public static IObservable CanNavigateBack(this IUseNavigation @this) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + return Observable.Create(obs => + { + var dis = new CompositeDisposable(); + @this.WhenSetup().Subscribe(_ => + { + if (NavigationHost.Count > 0 && @this.Name != null) + { + if (@this.Name.Length == 0) + { + NavigationHost.First().Value.CanNavigateBackObservable + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + + if (NavigationHost.TryGetValue(@this.Name, out var value)) + { + value.CanNavigateBackObservable + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + } + }); + + obs.OnNext(false); + + return dis; + }); + } + + /// + /// Determines whether this instance [can navigate back] the specified host name. + /// + /// The navigation host. + /// Name of the host. + /// + /// A bool. + /// + /// this. + public static IObservable CanNavigateBack(this IUseHostedNavigation @this, string hostName = "") + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + return Observable.Create(obs => + { + var dis = new CompositeDisposable(); + @this.WhenSetup(hostName).Subscribe(_ => + { + if (NavigationHost.Count > 0 && hostName != null) + { + if (hostName.Length == 0) + { + NavigationHost.First().Value.CanNavigateBackObservable + .DistinctUntilChanged() + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + else if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.CanNavigateBackObservable + .DistinctUntilChanged() + .Subscribe(x => obs.OnNext(x)) + .DisposeWith(dis); + } + } + }).DisposeWith(dis); + + obs.OnNext(false); + + return dis; + }); + } + + /// + /// Clears the history. + /// + /// The dummy. + /// + /// Chainable host. + /// + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation ClearHistory(this IUseNavigation @this) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.ClearHistory(); + break; + default: + NavigationHost[@this.Name].ClearHistory(); + break; + } + } + + return @this; + } + + /// + /// Clears the history. + /// + /// The this. + /// Name of the host. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation ClearHistory(this IUseHostedNavigation @this, string hostName = "") + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.ClearHistory(); + break; + default: + NavigationHost[hostName].ClearHistory(); + break; + } + } + + return @this; + } + + /// + /// Navigates the back. + /// + /// The this. + /// The parameter. + /// + /// Chainable host. + /// + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation NavigateBack(this IUseNavigation @this, object? parameter = null) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.NavigateBack(parameter); + break; + default: + if (NavigationHost.TryGetValue(@this.Name, out var value)) + { + value.NavigateBack(parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates backwards. + /// + /// The this. + /// Name of the host. + /// The parameter. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation NavigateBack(this IUseHostedNavigation @this, string hostName = "", object? parameter = null) + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.NavigateBack(parameter); + break; + default: + if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.NavigateBack(parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates the specified contract. + /// + /// The Type. + /// The this. + /// The contract. + /// The parameter. + /// Chainable host. + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation NavigateToView(this IUseNavigation @this, string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.Navigate(contract, parameter); + break; + default: + NavigationHost[@this.Name].Navigate(contract, parameter); + break; + } + } + + return @this; + } + + /// + /// Navigates to view. + /// + /// The Type. + /// The this. + /// Name of the host. + /// The contract. + /// The parameter. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation NavigateToView(this IUseHostedNavigation @this, string? hostName = "", string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.Navigate(contract, parameter); + break; + default: + if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.Navigate(contract, parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates the and reset. + /// + /// The Type. + /// The this. + /// The contract. + /// The parameter. + /// Chainable host. + /// this. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseNavigation NavigateToViewAndClearHistory(this IUseNavigation @this, string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + NavigationHost.First().Value.NavigateAndReset(contract, parameter); + break; + default: + if (NavigationHost.TryGetValue(@this.Name, out var value)) + { + value.NavigateAndReset(contract, parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Navigates to view and clear history. + /// + /// The Type. + /// The this. + /// Name of the host. + /// The contract. + /// The parameter. + /// Chainable host. + /// No navigation host registered, please ensure that the NavigationShell has a Name. + public static IUseHostedNavigation NavigateToViewAndClearHistory(this IUseHostedNavigation @this, string hostName = "", string? contract = null, object? parameter = null) + where T : class, IRxNavBase + { + if (NavigationHost.Count == 0) + { + throw new InvalidOperationException("No navigation host registered, please ensure that the NavigationShell has a Name."); + } + + if (NavigationHost.Count > 0 && hostName != null) + { + switch (hostName.Length) + { + case 0: + NavigationHost.First().Value.NavigateAndReset(contract, parameter); + break; + default: + if (NavigationHost.TryGetValue(hostName, out var value)) + { + value.NavigateAndReset(contract, parameter); + } + + break; + } + } + + return @this; + } + + /// + /// Sets the main navigation host. + /// + /// The dummy. + /// The view host. + public static void SetMainNavigationHost(this ISetNavigation @this, IViewModelRoutedViewHost viewHost) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + if (viewHost == null) + { + throw new ArgumentNullException(nameof(viewHost)); + } + + if (NavigationHost.ContainsKey(@this.Name)) + { + return; + } + + WhenSetupSubjects.Add(@this.Name, new(1)); + NavigationHost.Add(@this.Name, viewHost); + CurrentViewDisposable.Add(@this.Name, new CompositeDisposable()); + ResultNavigating.Add(@this.Name, new Subject()); + + if (viewHost.RequiresSetup) + { + viewHost.Setup(); + } + + ASetupCompleted.OnNext(Unit.Default); + WhenSetupSubjects[@this.Name].OnNext(true); + } + + /// + /// Whens the navigated from. + /// + /// The this. + /// The e. + public static void WhenNavigatedFrom(this INotifiyNavigation @this, Action e) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + @this.ISetupNavigatedFrom = true; + var vm = (@this as IAmViewFor)?.ViewModel as INotifiyRoutableViewModel; + SetWhenNavigated.Where(x => x.From != null && x.From.Name == vm?.Name).Subscribe(ea => + { + e(ea); + ea.From?.WhenNavigatedFrom(ea); + }).DisposeWith(@this.CleanUp); + } + + /// + /// Whens the navigated to. + /// + /// The this. + /// The e. + public static void WhenNavigatedTo(this INotifiyNavigation @this, Action e) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + @this.ISetupNavigatedTo = true; + var vm = (@this as IAmViewFor)?.ViewModel as INotifiyRoutableViewModel; + SetWhenNavigated.Where(x => x?.To?.Name == vm?.Name).Subscribe(ea => + { + if (ea.NavigationType == NavigationType.New) + { + CurrentViewDisposable[ea.HostName]?.Dispose(); + CurrentViewDisposable[ea.HostName] = new CompositeDisposable(); + } + + e(ea, CurrentViewDisposable[ea.HostName]); + ea?.To?.WhenNavigatedTo(ea, CurrentViewDisposable[ea.HostName]); + }).DisposeWith(@this.CleanUp); + } + + /// + /// Called when [navigating]. + /// + /// The this. + /// + /// The instance containing the event data. + /// + public static void WhenNavigating(this INotifiyNavigation @this, Func e) + { + if (@this == null) + { + throw new ArgumentNullException(nameof(@this)); + } + + @this.ISetupNavigating = true; + var vm = (@this as IAmViewFor)?.ViewModel as INotifiyRoutableViewModel; + SetWhenNavigating.Where(x => x?.From == null || x.From.Name == vm?.Name).Subscribe(ea => + { + if (ea != null) + { + if (ea.From != null) + { + e(ea); + } + + ea.From?.WhenNavigating(ea); + + ResultNavigating[ea.HostName].OnNext(ea); + } + }).DisposeWith(@this.CleanUp); + } + + /// + /// Whens the activated. + /// + /// The this. + /// A Bool. + public static IObservable WhenSetup(this IUseNavigation @this) => + Observable.Create(obs => + { + var dis = new CompositeDisposable(); + ASetupCompleted.Subscribe(_ => + { + if (WhenSetupSubjects.Count > 0 && @this.Name != null) + { + switch (@this.Name.Length) + { + case 0: + { + WhenSetupSubjects.First().Value.Where(x => x).Subscribe(obs).DisposeWith(dis); + break; + } + + default: + if (NavigationHost.ContainsKey(@this.Name)) + { + WhenSetupSubjects[@this.Name].Where(x => x).Subscribe(obs).DisposeWith(dis); + } + + break; + } + } + }).DisposeWith(dis); + return dis; + }); + + /// + /// Whens the activated. + /// + /// The dummy. + /// Name of the host. + /// + /// A Bool. + /// + public static IObservable WhenSetup(this IUseHostedNavigation dummy, string? hostName = "") => + Observable.Create(obs => + { + var dis = new CompositeDisposable(); + ASetupCompleted.Subscribe(_ => + { + if (WhenSetupSubjects.Count > 0) + { + if (hostName?.Length > 0) + { + if (NavigationHost.ContainsKey(hostName)) + { + WhenSetupSubjects[hostName].Where(x => x).Subscribe(obs).DisposeWith(dis); + } + } + else + { + WhenSetupSubjects.First().Value.Where(x => x).Subscribe(obs).DisposeWith(dis); + } + } + }).DisposeWith(dis); + return dis; + }); +#pragma warning restore RCS1175 // Unused 'this' parameter. +} diff --git a/src/ViewModel.MAUI.Example/App.xaml b/src/ViewModel.MAUI.Example/App.xaml new file mode 100644 index 0000000..9c2bb09 --- /dev/null +++ b/src/ViewModel.MAUI.Example/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/src/ViewModel.MAUI.Example/App.xaml.cs b/src/ViewModel.MAUI.Example/App.xaml.cs new file mode 100644 index 0000000..25979ee --- /dev/null +++ b/src/ViewModel.MAUI.Example/App.xaml.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2019-2023 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 ReactiveMarbles.Locator; +using ReactiveMarbles.Mvvm; +using ReactiveMarbles.ViewModel.Core; + +namespace ViewModel.MAUI.Example; + +/// +/// App. +/// +/// +public partial class App : Application +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// To be added. + /// + public App() + { + InitializeComponent(); + ServiceLocator.Current().AddCoreRegistrations(() => + CoreRegistrationBuilder + .Create() + .UseMauiThreadSchedulers() + .WithExceptionHandler(new DebugExceptionHandler()) + .Build()); + ServiceLocator.Current().AddSingleton(() => new()); + ServiceLocator.Current().AddNavigationView(); + + ServiceLocator.Current().AddSingleton(() => new()); + ServiceLocator.Current().AddNavigationView(); + + MainPage = new AppShell(); + ServiceLocator.Current().SetupComplete(); + } +} diff --git a/src/ViewModel.MAUI.Example/AppShell.xaml b/src/ViewModel.MAUI.Example/AppShell.xaml new file mode 100644 index 0000000..3606390 --- /dev/null +++ b/src/ViewModel.MAUI.Example/AppShell.xaml @@ -0,0 +1,13 @@ + + + + + + diff --git a/src/ViewModel.MAUI.Example/AppShell.xaml.cs b/src/ViewModel.MAUI.Example/AppShell.xaml.cs new file mode 100644 index 0000000..88c600b --- /dev/null +++ b/src/ViewModel.MAUI.Example/AppShell.xaml.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2019-2023 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. + +namespace ViewModel.MAUI.Example; + +/// +/// AppShell. +/// +/// +public partial class AppShell +{ + /// + /// Initializes a new instance of the class. + /// + public AppShell() + { + InitializeComponent(); + } +} diff --git a/src/ViewModel.MAUI.Example/MauiProgram.cs b/src/ViewModel.MAUI.Example/MauiProgram.cs new file mode 100644 index 0000000..88c7084 --- /dev/null +++ b/src/ViewModel.MAUI.Example/MauiProgram.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2019-2023 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. + +#if DEBUG +using Microsoft.Extensions.Logging; +#endif + +namespace ViewModel.MAUI.Example; + +/// +/// MauiProgram. +/// +public static class MauiProgram +{ + /// + /// Creates the maui application. + /// + /// A MauiApp. + public static MauiApp CreateMauiApp() + { + var builder = MauiApp.CreateBuilder(); + builder + .UseMauiApp() + .ConfigureFonts(fonts => + { + fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); + fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold"); + }); + +#if DEBUG + builder.Logging.AddDebug(); +#endif + + return builder.Build(); + } +} diff --git a/src/ViewModel.MAUI.Example/Platforms/Android/AndroidManifest.xml b/src/ViewModel.MAUI.Example/Platforms/Android/AndroidManifest.xml new file mode 100644 index 0000000..e9937ad --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Android/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Platforms/Android/MainActivity.cs b/src/ViewModel.MAUI.Example/Platforms/Android/MainActivity.cs new file mode 100644 index 0000000..cc5df13 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Android/MainActivity.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2019-2023 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 Android.App; +using Android.Content.PM; + +namespace ViewModel.MAUI.Example +{ + /// + /// MainActivity. + /// + /// + [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] + public class MainActivity : MauiAppCompatActivity + { + } +} diff --git a/src/ViewModel.MAUI.Example/Platforms/Android/MainApplication.cs b/src/ViewModel.MAUI.Example/Platforms/Android/MainApplication.cs new file mode 100644 index 0000000..034e10a --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Android/MainApplication.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2019-2023 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 Android.App; +using Android.Runtime; + +namespace ViewModel.MAUI.Example +{ + /// + /// MainApplication. + /// + /// + [Application] + public class MainApplication : MauiApplication + { + /// + /// Initializes a new instance of the class. + /// + /// The handle. + /// The ownership. + public MainApplication(IntPtr handle, JniHandleOwnership ownership) + : base(handle, ownership) + { + } + + /// + /// Creates the maui application. + /// + /// A MauiApp. + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + } +} diff --git a/src/ViewModel.MAUI.Example/Platforms/Android/Resources/values/colors.xml b/src/ViewModel.MAUI.Example/Platforms/Android/Resources/values/colors.xml new file mode 100644 index 0000000..c04d749 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Android/Resources/values/colors.xml @@ -0,0 +1,6 @@ + + + #512BD4 + #2B0B98 + #2B0B98 + \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/AppDelegate.cs b/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/AppDelegate.cs new file mode 100644 index 0000000..f9f1144 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/AppDelegate.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2019-2023 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 Foundation; + +namespace ViewModel.MAUI.Example; + +/// +/// AppDelegate. +/// +[Register("AppDelegate")] +public class AppDelegate : MauiUIApplicationDelegate +{ + /// + /// Creates the maui application. + /// + /// A MauiApp. + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); +} diff --git a/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/Info.plist b/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/Info.plist new file mode 100644 index 0000000..c96dd0a --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/Info.plist @@ -0,0 +1,30 @@ + + + + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/Program.cs b/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/Program.cs new file mode 100644 index 0000000..cbc81db --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/MacCatalyst/Program.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2019-2023 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 UIKit; + +namespace ViewModel.MAUI.Example; + +/// +/// Program. +/// +public static class Program +{ + /// + /// Defines the entry point of the application. + /// + /// The arguments. + private static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } +} diff --git a/src/ViewModel.MAUI.Example/Platforms/Tizen/Main.cs b/src/ViewModel.MAUI.Example/Platforms/Tizen/Main.cs new file mode 100644 index 0000000..600b3ee --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Tizen/Main.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Maui; +using Microsoft.Maui.Hosting; + +namespace ViewModel.MAUI.Example +{ + internal class Program : MauiApplication + { + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + + static void Main(string[] args) + { + var app = new Program(); + app.Run(args); + } + } +} \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Platforms/Tizen/tizen-manifest.xml b/src/ViewModel.MAUI.Example/Platforms/Tizen/tizen-manifest.xml new file mode 100644 index 0000000..fbf06ec --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Tizen/tizen-manifest.xml @@ -0,0 +1,15 @@ + + + + + + maui-appicon-placeholder + + + + + http://tizen.org/privilege/internet + + + + \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Platforms/Windows/App.xaml b/src/ViewModel.MAUI.Example/Platforms/Windows/App.xaml new file mode 100644 index 0000000..a4efc41 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Windows/App.xaml @@ -0,0 +1,8 @@ + + + diff --git a/src/ViewModel.MAUI.Example/Platforms/Windows/App.xaml.cs b/src/ViewModel.MAUI.Example/Platforms/Windows/App.xaml.cs new file mode 100644 index 0000000..e6b769f --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Windows/App.xaml.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2019-2023 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. + +namespace ViewModel.MAUI.Example.WinUI +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : MauiWinUIApplication + { + /// + /// Initializes a new instance of the class. + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + InitializeComponent(); + } + + /// + /// Creates the maui application. + /// + /// A MauiApp. + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + } +} diff --git a/src/ViewModel.MAUI.Example/Platforms/Windows/Package.appxmanifest b/src/ViewModel.MAUI.Example/Platforms/Windows/Package.appxmanifest new file mode 100644 index 0000000..395f179 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Windows/Package.appxmanifest @@ -0,0 +1,46 @@ + + + + + + + + + $placeholder$ + User Name + $placeholder$.png + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ViewModel.MAUI.Example/Platforms/Windows/app.manifest b/src/ViewModel.MAUI.Example/Platforms/Windows/app.manifest new file mode 100644 index 0000000..56ed24c --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/Windows/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/src/ViewModel.MAUI.Example/Platforms/iOS/AppDelegate.cs b/src/ViewModel.MAUI.Example/Platforms/iOS/AppDelegate.cs new file mode 100644 index 0000000..bf82ff1 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/iOS/AppDelegate.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2019-2023 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 Foundation; + +namespace ViewModel.MAUI.Example +{ + /// + /// AppDelegate. + /// + [Register("AppDelegate")] + public class AppDelegate : MauiUIApplicationDelegate + { + /// + /// Creates the maui application. + /// + /// A MauiApp. + protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); + } +} diff --git a/src/ViewModel.MAUI.Example/Platforms/iOS/Info.plist b/src/ViewModel.MAUI.Example/Platforms/iOS/Info.plist new file mode 100644 index 0000000..0004a4f --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/iOS/Info.plist @@ -0,0 +1,32 @@ + + + + + LSRequiresIPhoneOS + + UIDeviceFamily + + 1 + 2 + + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + XSAppIconAssets + Assets.xcassets/appicon.appiconset + + diff --git a/src/ViewModel.MAUI.Example/Platforms/iOS/Program.cs b/src/ViewModel.MAUI.Example/Platforms/iOS/Program.cs new file mode 100644 index 0000000..c43dd42 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Platforms/iOS/Program.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2019-2023 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 UIKit; + +namespace ViewModel.MAUI.Example +{ + /// + /// Program. + /// + public static class Program + { + /// + /// Defines the entry point of the application. + /// + /// The arguments. + private static void Main(string[] args) + { + // if you want to use a different Application Delegate class from "AppDelegate" + // you can specify it here. + UIApplication.Main(args, null, typeof(AppDelegate)); + } + } +} diff --git a/src/ViewModel.MAUI.Example/Properties/launchSettings.json b/src/ViewModel.MAUI.Example/Properties/launchSettings.json new file mode 100644 index 0000000..edf8aad --- /dev/null +++ b/src/ViewModel.MAUI.Example/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Windows Machine": { + "commandName": "MsixPackage", + "nativeDebugging": false + } + } +} \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Resources/AppIcon/appicon.svg b/src/ViewModel.MAUI.Example/Resources/AppIcon/appicon.svg new file mode 100644 index 0000000..9d63b65 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/AppIcon/appicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Resources/AppIcon/appiconfg.svg b/src/ViewModel.MAUI.Example/Resources/AppIcon/appiconfg.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/AppIcon/appiconfg.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Resources/Fonts/OpenSans-Regular.ttf b/src/ViewModel.MAUI.Example/Resources/Fonts/OpenSans-Regular.ttf new file mode 100644 index 0000000..60030d1 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/Fonts/OpenSans-Regular.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:094d3cbdb2ec1251eed65d2cf23d14bc89a5c50b81dbcbc36eb61deec403f090 +size 107124 diff --git a/src/ViewModel.MAUI.Example/Resources/Fonts/OpenSans-Semibold.ttf b/src/ViewModel.MAUI.Example/Resources/Fonts/OpenSans-Semibold.ttf new file mode 100644 index 0000000..a58f5e2 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/Fonts/OpenSans-Semibold.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96a3db2c9f845f680b0a4aab56763a74e5231bb2b5ed3d52114bc7226150c757 +size 111016 diff --git a/src/ViewModel.MAUI.Example/Resources/Images/dotnet_bot.svg b/src/ViewModel.MAUI.Example/Resources/Images/dotnet_bot.svg new file mode 100644 index 0000000..abfaff2 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/Images/dotnet_bot.svg @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ViewModel.MAUI.Example/Resources/Raw/AboutAssets.txt b/src/ViewModel.MAUI.Example/Resources/Raw/AboutAssets.txt new file mode 100644 index 0000000..15d6244 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/Raw/AboutAssets.txt @@ -0,0 +1,15 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories). Deployment of the asset to your application +is automatically handled by the following `MauiAsset` Build Action within your `.csproj`. + + + +These files will be deployed with you package and will be accessible using Essentials: + + async Task LoadMauiAsset() + { + using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt"); + using var reader = new StreamReader(stream); + + var contents = reader.ReadToEnd(); + } diff --git a/src/ViewModel.MAUI.Example/Resources/Splash/splash.svg b/src/ViewModel.MAUI.Example/Resources/Splash/splash.svg new file mode 100644 index 0000000..21dfb25 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/Splash/splash.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Resources/Styles/Colors.xaml b/src/ViewModel.MAUI.Example/Resources/Styles/Colors.xaml new file mode 100644 index 0000000..245758b --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/Styles/Colors.xaml @@ -0,0 +1,44 @@ + + + + + #512BD4 + #DFD8F7 + #2B0B98 + White + Black + #E1E1E1 + #C8C8C8 + #ACACAC + #919191 + #6E6E6E + #404040 + #212121 + #141414 + + + + + + + + + + + + + + + #F7B548 + #FFD590 + #FFE5B9 + #28C2D1 + #7BDDEF + #C3F2F4 + #3E8EED + #72ACF1 + #A7CBF6 + + \ No newline at end of file diff --git a/src/ViewModel.MAUI.Example/Resources/Styles/Styles.xaml b/src/ViewModel.MAUI.Example/Resources/Styles/Styles.xaml new file mode 100644 index 0000000..dc4a034 --- /dev/null +++ b/src/ViewModel.MAUI.Example/Resources/Styles/Styles.xaml @@ -0,0 +1,405 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ViewModel.MAUI.Example/ViewModel.MAUI.Example.csproj b/src/ViewModel.MAUI.Example/ViewModel.MAUI.Example.csproj new file mode 100644 index 0000000..34439b5 --- /dev/null +++ b/src/ViewModel.MAUI.Example/ViewModel.MAUI.Example.csproj @@ -0,0 +1,67 @@ + + + + net7.0-android;net7.0-ios;net7.0-maccatalyst + $(TargetFrameworks);net7.0-windows10.0.19041.0 + + + Exe + ViewModel.MAUI.Example + true + true + enable + enable + false + false + + + ViewModel.MAUI.Example + + + com.companyname.viewmodel.maui.example + c9081323-388e-460f-86b3-94e892a653cb + + + 1.0 + 1 + + 11.0 + 13.1 + 21.0 + 10.0.17763.0 + 10.0.17763.0 + 6.5 + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/src/ViewModel.MAUI.Example/ViewModels/FirstViewModel.cs b/src/ViewModel.MAUI.Example/ViewModels/FirstViewModel.cs new file mode 100644 index 0000000..ad959a2 --- /dev/null +++ b/src/ViewModel.MAUI.Example/ViewModels/FirstViewModel.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2019-2023 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.Diagnostics; +using System.Reactive; +using System.Reactive.Disposables; +using ReactiveMarbles.Command; +using ReactiveMarbles.ViewModel.Core; + +namespace ViewModel.MAUI.Example +{ + /// + /// FirstViewModel. + /// + public class FirstViewModel : RxNavBase + { + /// + /// Initializes a new instance of the class. + /// + public FirstViewModel() + { + GotoMain = RxCommand.Create(() => this.NavigateToView()); + + GotoFirst = RxCommand.Create(() => this.NavigateBack(), this.CanNavigateBack()); + } + + /// + /// Gets the goto main. + /// + /// + /// The goto main. + /// + public RxCommand GotoMain { get; } + + /// + /// Gets the goto first. + /// + /// + /// The goto first. + /// + public RxCommand GotoFirst { get; } + + /// + /// WhenNavigatedTo. + /// + /// + /// + /// + public override void WhenNavigatedTo(IViewModelNavigationEventArgs e, CompositeDisposable disposables) + { + if (e is null) + { + throw new ArgumentNullException(nameof(e)); + } + + Debug.WriteLine($"{DateTime.Now.ToString()} Navigated To: {e.To?.Name} From: {e.From?.Name} with Host {e.HostName}"); + base.WhenNavigatedTo(e, disposables); + } + + /// + /// WhenNavigatedFrom. + /// + /// + /// + public override void WhenNavigatedFrom(IViewModelNavigationEventArgs e) + { + if (e is null) + { + throw new ArgumentNullException(nameof(e)); + } + + Debug.WriteLine($"{DateTime.Now.ToString()} Navigated From: {e.From?.Name} To: {e.To?.Name} with Host {e.HostName}"); + base.WhenNavigatedFrom(e); + } + + /// + /// WhenNavigating. + /// + /// + /// + public override void WhenNavigating(IViewModelNavigatingEventArgs e) + { + if (e is null) + { + throw new ArgumentNullException(nameof(e)); + } + + Debug.WriteLine($"{DateTime.Now.ToString()} Navigating From: {e.From?.Name} To: {e.To?.Name} with Host {e.HostName}"); + base.WhenNavigating(e); + } + } +} diff --git a/src/ViewModel.MAUI.Example/ViewModels/MainViewModel.cs b/src/ViewModel.MAUI.Example/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..07c5bad --- /dev/null +++ b/src/ViewModel.MAUI.Example/ViewModels/MainViewModel.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2019-2023 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.Diagnostics; +using System.Reactive; +using System.Reactive.Disposables; +using ReactiveMarbles.Command; +using ReactiveMarbles.ViewModel.Core; +using RxNavBase = ReactiveMarbles.ViewModel.Core.RxNavBase; + +namespace ViewModel.MAUI.Example +{ + /// + /// MainViewModel. + /// + public class MainViewModel : RxNavBase + { + /// + /// Initializes a new instance of the class. + /// + public MainViewModel() + { + GotoFirst = RxCommand.Create(() => this.NavigateToView()); + + GotoMain = RxCommand.Create(() => this.NavigateBack(), this.CanNavigateBack()); + } + + /// + /// Gets the goto first. + /// + /// + /// The goto first. + /// + public RxCommand GotoFirst { get; } + + /// + /// Gets the goto main. + /// + /// + /// The goto main. + /// + public RxCommand GotoMain { get; } + + /// + /// WhenNavigatedTo. + /// + /// + /// + /// + public override void WhenNavigatedTo(IViewModelNavigationEventArgs e, CompositeDisposable disposables) + { + if (e == null) + { + throw new ArgumentNullException(nameof(e)); + } + + Debug.WriteLine($"{DateTime.Now.ToString()} Navigated To: {e.To?.Name} From: {e.From?.Name} with Host {e.HostName}"); + base.WhenNavigatedTo(e, disposables); + } + + /// + /// WhenNavigatedFrom. + /// + /// + /// + public override void WhenNavigatedFrom(IViewModelNavigationEventArgs e) + { + if (e == null) + { + throw new ArgumentNullException(nameof(e)); + } + + Debug.WriteLine($"{DateTime.Now.ToString()} Navigated From: {e.From?.Name} To: {e.To?.Name} with Host {e.HostName}"); + base.WhenNavigatedFrom(e); + } + + /// + /// WhenNavigating. + /// + /// + /// + public override void WhenNavigating(IViewModelNavigatingEventArgs e) + { + if (e == null) + { + throw new ArgumentNullException(nameof(e)); + } + + Debug.WriteLine($"{DateTime.Now.ToString()} Navigating From: {e.From?.Name!} To: {e.To?.Name!} with Host {e.HostName!}"); + base.WhenNavigating(e); + } + } +} diff --git a/src/ViewModel.MAUI.Example/Views/FirstView.xaml b/src/ViewModel.MAUI.Example/Views/FirstView.xaml new file mode 100644 index 0000000..501005b --- /dev/null +++ b/src/ViewModel.MAUI.Example/Views/FirstView.xaml @@ -0,0 +1,16 @@ + + + + +