├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── dotnet-format-daily.yml ├── .gitignore ├── .runsettings ├── AsyncAwaitBestPractices.sln ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE.md ├── README.md ├── azure-pipelines.yml ├── global.json ├── sample ├── .editorconfig ├── App.cs ├── AppShell.cs ├── Constants │ ├── ColorConstants.cs │ ├── EmojiConstants.cs │ ├── PageTitleConstants.cs │ └── StoriesConstants.cs ├── HackerNews.csproj ├── MauiProgram.cs ├── Models │ └── StoryModel.cs ├── Pages │ ├── Base │ │ └── BaseContentPage.cs │ ├── NewsPage.cs │ └── ShellRenderer.macios.cs ├── Platforms │ ├── Android │ │ ├── AndroidManifest.xml │ │ ├── MainActivity.cs │ │ ├── MainApplication.cs │ │ └── Resources │ │ │ └── values │ │ │ ├── colors.xml │ │ │ └── styles.xml │ ├── MacCatalyst │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ └── Program.cs │ ├── Windows │ │ ├── App.xaml │ │ ├── App.xaml.cs │ │ ├── Package.appxmanifest │ │ └── app.manifest │ └── iOS │ │ ├── AppDelegate.cs │ │ ├── Info.plist │ │ ├── Program.cs │ │ └── Resources │ │ └── LaunchScreen.xib ├── Resources │ ├── Fonts │ │ └── OpenSans-Regular.ttf │ ├── Images │ │ └── dotnet_bot.svg │ ├── appicon.svg │ └── appiconfg.svg ├── Services │ ├── EnumerableExtensions.cs │ ├── HackerNewsAPIService.cs │ ├── IHackerNewsAPI.cs │ └── ServiceProvider.cs ├── ViewModels │ ├── BaseViewModel.cs │ ├── NewsViewModel_BadAsyncAwaitPractices.cs │ └── NewsViewModel_GoodAsyncAwaitPractices.cs └── Views │ └── News │ └── StoryDataTemplate.cs └── src ├── AsyncAwaitBestPractices.MVVM ├── .editorconfig ├── AsyncAwaitBestPractices.MVVM.csproj ├── AsyncAwaitBestPracticesMVVM.snk ├── AsyncCommand │ ├── AsyncCommand.shared.cs │ ├── BaseAsyncCommand.shared.cs │ └── IAsyncCommand.shared.cs ├── AsyncValueCommand │ ├── AsyncValueCommand.shared.cs │ ├── BaseAsyncValueCommand.shared.cs │ └── IAsyncValueCommand.shared.cs ├── BaseCommand.shared.cs ├── InvalidCommandParameterException.shared.cs └── README.md ├── AsyncAwaitBestPractices.UnitTests ├── AsyncAwaitBestPractices.UnitTests.csproj ├── AsyncAwaitBestPracticesUnitTests.snk ├── BaseTest.cs ├── CommandTests │ ├── AsyncCommand │ │ ├── BaseAsyncCommandTest.cs │ │ ├── Tests_AsyncCommand.cs │ │ ├── Tests_IAsyncCommand.cs │ │ └── Tests_ICommand_AsyncCommand.cs │ └── AsyncValueCommand │ │ ├── BaseAsyncValueCommandTest.cs │ │ ├── Tests_AsyncValueCommand.cs │ │ ├── Tests_IAsyncValueCommand.cs │ │ └── Tests_ICommand_AsyncValueCommand.cs ├── SafeFireAndForgetTests │ ├── Tests_Task_SafeFireAndForget.cs │ ├── Tests_Task_SafeFireAndForgetT.cs │ ├── Tests_Task_SafeFireAndForgetT_ConfigureAwaitOptions.cs │ ├── Tests_Task_SafeFireAndForget_ConfigureAwaitOptions.cs │ ├── Tests_ValueTask_SafeFIreAndForgetT.cs │ └── Tests_ValueTask_SafeFireAndForget.cs └── WeakEventManagerTests │ ├── Tests_WeakEventManager_Action.cs │ ├── Tests_WeakEventManager_ActionT.cs │ ├── Tests_WeakEventManager_Delegate.cs │ ├── Tests_WeakEventManager_EventHandler.cs │ └── Tests_WeakEventManager_EventHandlerT.cs └── AsyncAwaitBestPractices ├── .editorconfig ├── AsyncAwaitBestPractices.csproj ├── AsyncAwaitBestPractices.snk ├── InvalidHandleEventException.shared.cs ├── Properties └── AssemblyInfo.cs ├── README.md ├── SafeFireAndForgetExtensions.extensions.shared.cs ├── SafeFireAndForgetExtensions.shared.cs └── WeakEventManager ├── EventManagerService.shared.cs ├── Subscription.shared.cs ├── WeakEventManager.extensions.shared.cs └── WeakEventManager.shared.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # Suppress: EC112 2 | # top-most EditorConfig file 3 | root = true 4 | 5 | # Default settings: 6 | # A newline ending every file 7 | # Use 4 spaces as indentation 8 | [*] 9 | insert_final_newline = false 10 | indent_style = space 11 | indent_size = 4 12 | 13 | # Code files 14 | [*.{cs,csx,vb,vbx}] 15 | indent_style = tab 16 | indent_size = 4 17 | 18 | # Code files 19 | [*.sln] 20 | indent_size = 4 21 | 22 | # Xml project files 23 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 24 | indent_size = 2 25 | 26 | # Xml config files 27 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 28 | indent_size = 2 29 | 30 | # JSON files 31 | [*.json] 32 | indent_size = 2 33 | 34 | # XML files 35 | [*.xml] 36 | indent_size = 2 37 | 38 | # Dotnet code style settings: 39 | [*.{cs,vb}] 40 | # IDE0005: Remove unnecessary imports 41 | dotnet_diagnostic.IDE0005.severity = suggestion 42 | # Sort using and Import directives with System.* appearing first 43 | dotnet_sort_system_directives_first = true 44 | # Avoid "this." and "Me." if not necessary 45 | dotnet_style_qualification_for_field = false:suggestion 46 | dotnet_style_qualification_for_property = false:suggestion 47 | dotnet_style_qualification_for_method = false:suggestion 48 | dotnet_style_qualification_for_event = false:suggestion 49 | 50 | # Use language keywords instead of framework type names for type references 51 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 52 | dotnet_style_predefined_type_for_member_access = true:suggestion 53 | dotnet_style_require_accessibility_modifiers = omit_if_default:warning 54 | 55 | # Suggest more modern language features when available 56 | dotnet_style_object_initializer = true:suggestion 57 | dotnet_style_collection_initializer = true:suggestion 58 | dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion 59 | dotnet_style_coalesce_expression = true:suggestion 60 | dotnet_style_null_propagation = true:suggestion 61 | dotnet_style_explicit_tuple_names = true:suggestion 62 | 63 | # Naming Conventions: 64 | # Pascal Casing 65 | dotnet_naming_symbols.method_and_property_symbols.applicable_kinds= method,property,enum 66 | dotnet_naming_symbols.method_and_property_symbols.applicable_accessibilities = * 67 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 68 | 69 | dotnet_naming_rule.methods_and_properties_must_be_pascal_case.severity = warning 70 | dotnet_naming_rule.methods_and_properties_must_be_pascal_case.symbols = method_and_property_symbols 71 | dotnet_naming_rule.methods_and_properties_must_be_pascal_case.style = pascal_case_style 72 | 73 | # Non-public members must be lower-case 74 | dotnet_naming_symbols.non_public_symbols.applicable_kinds = field 75 | dotnet_naming_symbols.non_public_symbols.applicable_accessibilities = private 76 | #dotnet_naming_style.all_lower_case_style.capitalization = camel_case 77 | 78 | dotnet_naming_rule.non_public_members_must_be_lower_case.severity = warning 79 | dotnet_naming_rule.non_public_members_must_be_lower_case.symbols = non_public_symbols 80 | dotnet_naming_rule.non_public_members_must_be_lower_case.style = all_lower_case_style 81 | 82 | dotnet_diagnostic.IDE0002.severity = error 83 | 84 | # Organize usings 85 | dotnet_sort_system_directives_first = true 86 | 87 | # CS4014: Because this call is not awaited, execution of the current method continues before the call is completed 88 | dotnet_diagnostic.CS4014.severity = error 89 | 90 | # Remove explicit default access modifiers 91 | dotnet_style_require_accessibility_modifiers = omit_if_default:error 92 | 93 | # CA1063: Implement IDisposable Correctly 94 | dotnet_diagnostic.CA1063.severity = error 95 | 96 | # CA1707: Remove the underscores from member name 97 | dotnet_diagnostic.CA1707.severity = none 98 | 99 | # CA1305: The behavior of 'string.Format(string, object)' could vary based on the current user's locale settings. Replace this call 100 | dotnet_diagnostic.CA1305.severity = suggestion 101 | 102 | # CA1822: Member does not access instance data and can be marked as static 103 | dotnet_diagnostic.CA1822.severity = error 104 | 105 | # CA1050: Declar types in namespaces 106 | dotnet_diagnostic.CA1050.severity = error 107 | 108 | # CA2016: Forward the 'cancellationToken' parameter to the 'Delay' method or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token 109 | dotnet_diagnostic.CA2016.severity = error 110 | 111 | # CA2208: Method passes parameter as the paramName argument to a ArgumentNullException constructor. Replace this argument with one of the method's parameter names. Note that the provided parameter name should have the exact casing as declared on the method. 112 | dotnet_diagnostic.CA2208.severity = error 113 | 114 | # CA1001: Type owns disposable field(s) but is not disposable 115 | dotnet_diagnostic.CA1001.severity = error 116 | 117 | # CA1834: Use 'StringBuilder.Append(char)' instead of 'StringBuilder.Append(string)' when the input is a constant unit string 118 | dotnet_diagnostic.CA1834.severity = error 119 | 120 | # CA1309: Use ordinal string comparison 121 | dotnet_diagnostic.CA1309.severity = error 122 | 123 | # CSharp code style settings: 124 | [*.cs] 125 | # Do not prefer "var" everywhere 126 | csharp_style_var_for_built_in_types = true:none 127 | csharp_style_var_when_type_is_apparent = true:none 128 | csharp_style_var_elsewhere = true:none 129 | 130 | # Prefer method-like constructs to have a block body 131 | csharp_style_expression_bodied_methods = true:suggestion 132 | csharp_style_expression_bodied_constructors = true:suggestion 133 | csharp_style_expression_bodied_operators = true:suggestion 134 | 135 | # Prefer property-like constructs to have an expression-body 136 | csharp_style_expression_bodied_properties = true:suggestion 137 | csharp_style_expression_bodied_indexers = true:suggestion 138 | csharp_style_expression_bodied_accessors = true:suggestion 139 | 140 | # Suggest more modern language features when available 141 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 142 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 143 | csharp_style_inlined_variable_declaration = true:suggestion 144 | csharp_style_throw_expression = true:suggestion 145 | csharp_style_conditional_delegate_call = true:suggestion 146 | 147 | # Newline settings 148 | csharp_new_line_before_open_brace = all 149 | csharp_new_line_before_else = true 150 | csharp_new_line_before_catch = true 151 | csharp_new_line_before_finally = true 152 | csharp_new_line_before_members_in_object_initializers = true 153 | csharp_new_line_before_members_in_anonymous_types = true 154 | 155 | #IDE0160: Use file scoped namespace 156 | csharp_style_namespace_declarations = file_scoped:error -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | *.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | 65 | # Force bash scripts to always use lf line endings so that if a repo is accessed 66 | # in Unix via a file share from Windows, the scripts will work. 67 | *.sh text eol=lf 68 | 69 | # Force the docs to always use lf line endings 70 | docs/**/*.xml text eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [brminnick] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | - package-ecosystem: "dotnet-sdk" 13 | directory: "/" 14 | schedule: 15 | interval: "weekly" 16 | day: "wednesday" 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | day: "wednesday" 22 | -------------------------------------------------------------------------------- /.github/dotnet-format-daily.yml: -------------------------------------------------------------------------------- 1 | name: Daily code format check 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * # Every day at midnight (UTC) 5 | jobs: 6 | dotnet-format: 7 | runs-on: windows-latest 8 | steps: 9 | - name: Install dotnet-format 10 | run: dotnet tool install -g dotnet-format 11 | 12 | - name: Checkout repo 13 | uses: actions/checkout@v3 14 | with: 15 | ref: ${{ github.head_ref }} 16 | 17 | - name: Run dotnet format 18 | id: format 19 | uses: jfversluis/dotnet-format@v1.0.5 20 | with: 21 | repo-token: ${{ secrets.GITHUB_TOKEN }} 22 | action: "fix" 23 | #only-changed-files: true # only works for PRs 24 | workspace: "samples/CommunityToolkit.Maui.Sample.sln" 25 | 26 | - name: Commit files 27 | if: steps.format.outputs.has-changes == 'true' 28 | run: | 29 | git config --local user.name "github-actions[bot]" 30 | git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 31 | git commit -a -m 'Automated dotnet-format update' 32 | 33 | - name: Create Pull Request 34 | uses: peter-evans/create-pull-request@v3 35 | with: 36 | title: '[housekeeping] Automated PR to fix formatting errors' 37 | body: | 38 | Automated PR to fix formatting errors 39 | committer: GitHub 40 | author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> 41 | labels: approved 42 | assignees: brminnick 43 | reviewers: brminnick 44 | branch: housekeeping/fix-codeformatting 45 | 46 | # Pushing won't work to forked repos 47 | # - name: Commit files 48 | # if: steps.format.outputs.has-changes == 'true' 49 | # run: | 50 | # git config --local user.name "github-actions[bot]" 51 | # git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" 52 | # git commit -a -m 'Automated dotnet-format update 53 | # Co-authored-by: ${{ github.event.pull_request.user.login }} <${{ github.event.pull_request.user.id }}+${{ github.event.pull_request.user.login }}@users.noreply.github.com>' 54 | 55 | # - name: Push changes 56 | # if: steps.format.outputs.has-changes == 'true' 57 | # uses: ad-m/github-push-action@v0.6.0 58 | # with: 59 | # github_token: ${{ secrets.GITHUB_TOKEN }} 60 | # branch: ${{ github.event.pull_request.head.ref }} -------------------------------------------------------------------------------- /.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | cobertura 12 | true 13 | false 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AsyncAwaitBestPractices.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.1.31903.286 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample", "Sample", "{2A09E240-E14C-45D9-8BE7-86EC6A5007DE}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{3CDAB3BD-D9EA-4BCD-8FA8-C103F2136337}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAwaitBestPractices.UnitTests", "src\AsyncAwaitBestPractices.UnitTests\AsyncAwaitBestPractices.UnitTests.csproj", "{D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}" 10 | EndProject 11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAwaitBestPractices", "src\AsyncAwaitBestPractices\AsyncAwaitBestPractices.csproj", "{C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AsyncAwaitBestPractices.MVVM", "src\AsyncAwaitBestPractices.MVVM\AsyncAwaitBestPractices.MVVM.csproj", "{7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D59772E1-5AE1-4B9C-B193-AEB40CD697E3}" 16 | ProjectSection(SolutionItems) = preProject 17 | .editorconfig = .editorconfig 18 | Directory.Build.props = Directory.Build.props 19 | global.json = global.json 20 | Directory.Build.targets = Directory.Build.targets 21 | EndProjectSection 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HackerNews", "sample\HackerNews.csproj", "{FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}" 24 | EndProject 25 | Global 26 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 27 | Debug|Any CPU = Debug|Any CPU 28 | Debug|iPhone = Debug|iPhone 29 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 30 | Release|Any CPU = Release|Any CPU 31 | Release|iPhone = Release|iPhone 32 | Release|iPhoneSimulator = Release|iPhoneSimulator 33 | EndGlobalSection 34 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 35 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Debug|iPhone.ActiveCfg = Debug|Any CPU 38 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Debug|iPhone.Build.0 = Debug|Any CPU 39 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 40 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 41 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Release|iPhone.ActiveCfg = Release|Any CPU 44 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Release|iPhone.Build.0 = Release|Any CPU 45 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 46 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 47 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Debug|iPhone.ActiveCfg = Debug|Any CPU 50 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Debug|iPhone.Build.0 = Debug|Any CPU 51 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 52 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 53 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Release|iPhone.ActiveCfg = Release|Any CPU 56 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Release|iPhone.Build.0 = Release|Any CPU 57 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 58 | {C52330DE-88C6-40A8-AA6E-DA5E7AE35D72}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 59 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Debug|iPhone.ActiveCfg = Debug|Any CPU 62 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Debug|iPhone.Build.0 = Debug|Any CPU 63 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 64 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 65 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Release|Any CPU.Build.0 = Release|Any CPU 67 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Release|iPhone.ActiveCfg = Release|Any CPU 68 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Release|iPhone.Build.0 = Release|Any CPU 69 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 70 | {7E7B9DEC-FDA8-43F9-9303-DA9FC27A9B15}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 71 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 72 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 73 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Debug|iPhone.ActiveCfg = Debug|Any CPU 74 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Debug|iPhone.Build.0 = Debug|Any CPU 75 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 76 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 77 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 78 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Release|Any CPU.Build.0 = Release|Any CPU 79 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Release|iPhone.ActiveCfg = Release|Any CPU 80 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Release|iPhone.Build.0 = Release|Any CPU 81 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 82 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 83 | EndGlobalSection 84 | GlobalSection(SolutionProperties) = preSolution 85 | HideSolutionNode = FALSE 86 | EndGlobalSection 87 | GlobalSection(NestedProjects) = preSolution 88 | {D741AD98-D92C-4A4A-94D7-0E4F55C7CAA1} = {3CDAB3BD-D9EA-4BCD-8FA8-C103F2136337} 89 | {FCD28BE8-BFDF-4316-9C21-64CD8A00B9A5} = {2A09E240-E14C-45D9-8BE7-86EC6A5007DE} 90 | EndGlobalSection 91 | GlobalSection(ExtensibilityGlobals) = postSolution 92 | SolutionGuid = {BBC2B0A7-879B-4A66-8228-3E897F3BA5DA} 93 | EndGlobalSection 94 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 95 | sample\HackerNews.Shared\HackerNews.Shared.projitems*{8551b218-5734-4f5c-9d35-25db859ccfde}*SharedItemsImports = 13 96 | sample\HackerNews.Shared\HackerNews.Shared.projitems*{c3d6de1f-ed08-4ca0-a092-56785bb3cb4d}*SharedItemsImports = 5 97 | sample\HackerNews.Shared\HackerNews.Shared.projitems*{cca4bf8b-7b64-4f7b-9c3a-ae498a65fd43}*SharedItemsImports = 4 98 | EndGlobalSection 99 | EndGlobal 100 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | enable 5 | 9.0.0 6 | preview 7 | true 8 | net9.0 9 | true 10 | true 11 | true 12 | false 13 | 14 | 15 | 9.0.30 16 | true 17 | true 18 | true 19 | 20 | 21 | enable 22 | all 23 | 24 | 25 | false 26 | false 27 | 28 | 64 | 65 | nullable, 66 | CS0419,CS0618,CS1570,CS1571,CS1572,CS1573,CS1574,CS1580,CS1581,CS1584,CS1587,CS1589,CS1590,CS1591,CS1592,CS1598,CS1658,CS1710,CS1711,CS1712,CS1723,CS1734, 67 | CsWinRT1028,CsWinRT1030, 68 | XC0045,XC0103, 69 | NU1900,NU1901,NU1902,NU1903,NU1904,NU1905, 70 | NUnit1001,NUnit1002,NUnit1003,NUnit1004,NUnit1005,NUnit1006,NUnit1007,NUnit1008,NUnit1009,NUnit1010,NUnit1011,NUnit1012,NUnit1013,NUnit1014,NUnit1015,NUnit1016,NUnit1017,NUnit1018,NUnit1019,NUnit1020,NUnit1021,NUnit1022,NUnit1023,NUnit1024,NUnit1025,NUnit1026,NUnit1027,NUnit1028,NUnit1029,NUnit1030,NUnit1031,NUnit1032,NUnit1033, 71 | NUnit2001,NUnit2002,NUnit2003,NUnit2004,NUnit2005,NUnit2006,NUnit2007,NUnit2008,NUnit2009,NUnit2010,NUnit2011,NUnit2012,NUnit2013,NUnit2014,NUnit2015,NUnit2016,NUnit2017,NUnit2018,NUnit2019,NUnit2020,NUnit2021,NUnit2022,NUnit2023,NUnit2024,NUnit2025,NUnit2026,NUnit2027,NUnit2028,NUnit2029,NUnit2030,NUnit2031,NUnit2032,NUnit2033,NUnit2034,NUnit2035,NUnit2036,NUnit2037,NUnit2038,NUnit2039,NUnit2040,NUnit2041,NUnit2042,NUnit2043,NUnit2044,NUnit2045,NUnit2046,NUnit2047,NUnit2048,NUnit2049,NUnit2050, 72 | NUnit3001,NUnit3002,NUnit3003,NUnit3004, 73 | NUnit4001, 74 | IL2001,IL2002,IL2003,IL2004,IL2005,IL2006,IL2007,IL2008,IL2009, 75 | IL2010,IL2011,IL2012,IL2013,IL2014,IL2015,IL2016,IL2017,IL2018,IL2019, 76 | IL2020,IL2021,IL2022,IL2023,IL2024,IL2025,IL2026,IL2027,IL2028,IL2029, 77 | IL2030,IL2031,IL2032,IL2033,IL2034,IL2035,IL2036,IL2037,IL2038,IL2039, 78 | IL2040,IL2041,IL2042,IL2043,IL2044,IL2045,IL2046,IL2047,IL2048,IL2049, 79 | IL2050,IL2051,IL2052,IL2053,IL2054,IL2055,IL2056,IL2057,IL2058,IL2059, 80 | IL2060,IL2061,IL2062,IL2063,IL2064,IL2065,IL2066,IL2067,IL2068,IL2069, 81 | IL2070,IL2071,IL2072,IL2073,IL2074,IL2075,IL2076,IL2077,IL2078,IL2079, 82 | IL2080,IL2081,IL2082,IL2083,IL2084,IL2085,IL2086,IL2087,IL2088,IL2089, 83 | IL2090,IL2091,IL2092,IL2093,IL2094,IL2095,IL2096,IL2097,IL2098,IL2099, 84 | IL2100,IL2101,IL2102,IL2103,IL2104,IL2105,IL2106,IL2107,IL2108,IL2109, 85 | IL2110,IL2111,IL2112,IL2113,IL2114,IL2115,IL2116,IL2117,IL2118,IL2119, 86 | IL2120,IL2121,IL2122, 87 | IL3050,IL3051,IL3052,IL3053,IL3054,IL3055,IL3056 88 | 89 | 90 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Brandon Minnick 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.300", 4 | "rollForward": "latestFeature", 5 | "allowPrerelease": false 6 | } 7 | } -------------------------------------------------------------------------------- /sample/.editorconfig: -------------------------------------------------------------------------------- 1 | root = false 2 | 3 | [*.cs] 4 | # CS4014: Because this call is not awaited, execution of the current method continues before the call is completed 5 | dotnet_diagnostic.CS4014.severity = warning 6 | # CA2016: Forward the 'cancellationToken' parameter to the 'Delay' method or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token 7 | dotnet_diagnostic.CA2016.severity = warning -------------------------------------------------------------------------------- /sample/App.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Maui.Markup; 2 | using Microsoft.Maui.Controls; 3 | 4 | namespace HackerNews; 5 | 6 | partial class App : Application 7 | { 8 | readonly AppShell _appShell; 9 | 10 | public App(AppShell appShell) 11 | { 12 | Resources = new ResourceDictionary 13 | { 14 | new Style( 15 | (Shell.NavBarHasShadowProperty, true), 16 | (Shell.TitleColorProperty, ColorConstants.NavigationBarTextColor), 17 | (Shell.DisabledColorProperty, ColorConstants.NavigationBarTextColor), 18 | (Shell.UnselectedColorProperty, ColorConstants.NavigationBarTextColor), 19 | (Shell.ForegroundColorProperty, ColorConstants.NavigationBarTextColor), 20 | (Shell.BackgroundColorProperty, ColorConstants.NavigationBarBackgroundColor)).ApplyToDerivedTypes(true), 21 | 22 | new Style( 23 | (NavigationPage.BarTextColorProperty, ColorConstants.NavigationBarTextColor), 24 | (NavigationPage.BarBackgroundColorProperty, ColorConstants.NavigationBarBackgroundColor)).ApplyToDerivedTypes(true) 25 | }; 26 | 27 | _appShell = appShell; 28 | } 29 | 30 | protected override Window CreateWindow(IActivationState? activationState) => new(_appShell); 31 | } -------------------------------------------------------------------------------- /sample/AppShell.cs: -------------------------------------------------------------------------------- 1 | namespace HackerNews; 2 | 3 | partial class AppShell : Shell 4 | { 5 | public AppShell(NewsPage newsPage) 6 | { 7 | Items.Add(newsPage); 8 | 9 | #if IOS || MACCATALYST 10 | ShellAttachedProperties.SetPrefersLargeTitles(this, true); 11 | #endif 12 | } 13 | } -------------------------------------------------------------------------------- /sample/Constants/ColorConstants.cs: -------------------------------------------------------------------------------- 1 | namespace HackerNews; 2 | 3 | static class ColorConstants 4 | { 5 | public static Color NavigationBarBackgroundColor { get; } = Color.FromArgb("FF6601"); 6 | public static Color NavigationBarTextColor { get; } = Colors.Black; 7 | 8 | public static Color TextCellDetailColor { get; } = Color.FromArgb("828282"); 9 | public static Color TextCellTextColor { get; } = Colors.Black; 10 | 11 | public static Color BrowserNavigationBarBackgroundColor { get; } = Color.FromArgb("FFE6D5"); 12 | public static Color BrowserNavigationBarTextColor { get; } = Color.FromArgb("3F3F3F"); 13 | } -------------------------------------------------------------------------------- /sample/Constants/EmojiConstants.cs: -------------------------------------------------------------------------------- 1 | namespace HackerNews; 2 | 3 | static class EmojiConstants 4 | { 5 | public const string SadFaceEmoji = "☹️"; 6 | public const string NeutralFaceEmoji = "\U0001F610"; 7 | public const string HappyFaceEmoji = "\U0001F603"; 8 | public const string BlankFaceEmoji = "\U0001F636"; 9 | } -------------------------------------------------------------------------------- /sample/Constants/PageTitleConstants.cs: -------------------------------------------------------------------------------- 1 | namespace HackerNews; 2 | 3 | static class PageTitleConstants 4 | { 5 | public const string NewsPageTitle = "Top Stories"; 6 | } -------------------------------------------------------------------------------- /sample/Constants/StoriesConstants.cs: -------------------------------------------------------------------------------- 1 | namespace HackerNews; 2 | 3 | static class StoriesConstants 4 | { 5 | public const int NumberOfStories = 50; 6 | } -------------------------------------------------------------------------------- /sample/HackerNews.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(LatestSupportedTFM)-ios;$(LatestSupportedTFM)-android;$(LatestSupportedTFM)-maccatalyst 5 | $(TargetFrameworks);$(LatestSupportedTFM)-windows10.0.19041.0 6 | Exe 7 | HackerNews 8 | true 9 | true 10 | enable 11 | false 12 | 13 | HackerNews 14 | 15 | com.Minnick.HackerNews 16 | da4c7bba-c5a8-4480-9feb-b4bd874144fb 17 | 18 | 1 19 | 20 | 15.0 21 | 15.0 22 | 25.0 23 | 10.0.19041.0 24 | 10.0.19041.0 25 | 26 | true 27 | iPhone Developer 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | maccatalyst-arm64;maccatalyst-x64 58 | 59 | 60 | 61 | ios-arm64 62 | 63 | 64 | -------------------------------------------------------------------------------- /sample/MauiProgram.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Maui; 2 | using CommunityToolkit.Maui.Markup; 3 | using Microsoft.Extensions.Http.Resilience; 4 | using Polly; 5 | using Refit; 6 | 7 | [assembly: XamlCompilation(XamlCompilationOptions.Compile)] 8 | namespace HackerNews; 9 | 10 | public static class MauiProgram 11 | { 12 | public static MauiApp CreateMauiApp() 13 | { 14 | var builder = MauiApp.CreateBuilder() 15 | .UseMauiApp() 16 | .UseMauiCommunityToolkit() 17 | .UseMauiCommunityToolkitMarkup(); 18 | 19 | builder.ConfigureMauiHandlers(handlers => 20 | { 21 | #if IOS || MACCATALYST 22 | handlers.AddHandler(); 23 | #endif 24 | }); 25 | 26 | // App 27 | builder.Services.AddSingleton(); 28 | builder.Services.AddSingleton(); 29 | 30 | // Services 31 | builder.Services.AddSingleton(Browser.Default); 32 | builder.Services.AddSingleton(); 33 | 34 | builder.Services.AddRefitClient() 35 | .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://hacker-news.firebaseio.com/v0")) 36 | .AddStandardResilienceHandler(options => options.Retry = new MobileHttpRetryStrategyOptions()); 37 | 38 | // Pages + View Models 39 | builder.Services.AddTransientWithShellRoute($"//{nameof(NewsPage)}"); 40 | 41 | return builder.Build(); 42 | } 43 | 44 | sealed class MobileHttpRetryStrategyOptions : HttpRetryStrategyOptions 45 | { 46 | public MobileHttpRetryStrategyOptions() 47 | { 48 | BackoffType = DelayBackoffType.Exponential; 49 | MaxRetryAttempts = 3; 50 | UseJitter = true; 51 | Delay = TimeSpan.FromMilliseconds(2); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /sample/Models/StoryModel.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace HackerNews; 4 | 5 | record StoryModel( 6 | [property: JsonPropertyName("by")] string Author, 7 | [property: JsonPropertyName("id")] long Id, 8 | [property: JsonPropertyName("score")] int Score, 9 | [property: JsonPropertyName("time")] long CreatedAt_UnixTime, 10 | [property: JsonPropertyName("title")] string Title, 11 | [property: JsonPropertyName("type")] string Type, 12 | [property: JsonPropertyName("url")] string Url) 13 | { 14 | public DateTimeOffset CreatedAt_DateTimeOffset { get; } = DateTimeOffset.FromUnixTimeSeconds(CreatedAt_UnixTime); 15 | 16 | public string Description => ToString(); 17 | 18 | public override string ToString() => $"{Score} Points by {Author}, {GetAgeOfStory(CreatedAt_DateTimeOffset)} ago"; 19 | 20 | static string GetAgeOfStory(in DateTimeOffset storyCreatedAt) 21 | { 22 | var timespanSinceStoryCreated = DateTimeOffset.UtcNow - storyCreatedAt; 23 | 24 | return timespanSinceStoryCreated switch 25 | { 26 | TimeSpan storyAge when storyAge < TimeSpan.FromHours(1) => $"{Math.Ceiling(timespanSinceStoryCreated.TotalMinutes)} minutes", 27 | 28 | TimeSpan storyAge when storyAge >= TimeSpan.FromHours(1) && storyAge < TimeSpan.FromHours(2) => $"{Math.Floor(timespanSinceStoryCreated.TotalHours)} hour", 29 | 30 | TimeSpan storyAge when storyAge >= TimeSpan.FromHours(2) && storyAge < TimeSpan.FromHours(24) => $"{Math.Floor(timespanSinceStoryCreated.TotalHours)} hours", 31 | 32 | TimeSpan storyAge when storyAge >= TimeSpan.FromHours(24) && storyAge < TimeSpan.FromHours(48) => $"{Math.Floor(timespanSinceStoryCreated.TotalDays)} day", 33 | 34 | TimeSpan storyAge when storyAge >= TimeSpan.FromHours(48) => $"{Math.Floor(timespanSinceStoryCreated.TotalDays)} days", 35 | 36 | _ => string.Empty, 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /sample/Pages/Base/BaseContentPage.cs: -------------------------------------------------------------------------------- 1 | namespace HackerNews; 2 | 3 | abstract partial class BaseContentPage : ContentPage where TViewModel : BaseViewModel 4 | { 5 | protected BaseContentPage(TViewModel viewModel, string pageTitle) 6 | { 7 | Title = pageTitle; 8 | base.BindingContext = viewModel; 9 | } 10 | 11 | protected new TViewModel BindingContext => (TViewModel)base.BindingContext; 12 | } -------------------------------------------------------------------------------- /sample/Pages/NewsPage.cs: -------------------------------------------------------------------------------- 1 | using CommunityToolkit.Maui.Markup; 2 | 3 | namespace HackerNews; 4 | 5 | partial class NewsPage : BaseContentPage 6 | { 7 | readonly IBrowser _browser; 8 | readonly IDispatcher _dispatcher; 9 | 10 | public NewsPage(IBrowser browser, 11 | IDispatcher dispatcher, 12 | NewsViewModel newsViewModel) : base(newsViewModel, "Top Stories") 13 | { 14 | _browser = browser; 15 | _dispatcher = dispatcher; 16 | 17 | BindingContext.PullToRefreshFailed += HandlePullToRefreshFailed; 18 | 19 | Content = new RefreshView 20 | { 21 | RefreshColor = Colors.Black, 22 | 23 | Content = new CollectionView 24 | { 25 | BackgroundColor = Color.FromArgb("F6F6EF"), 26 | SelectionMode = SelectionMode.Single, 27 | ItemTemplate = new StoryDataTemplate(), 28 | 29 | }.Bind(ItemsView.ItemsSourceProperty, 30 | getter: static (NewsViewModel vm) => vm.TopStoryCollection) 31 | .Invoke(collectionView => collectionView.SelectionChanged += HandleSelectionChanged) 32 | 33 | }.Bind(RefreshView.IsRefreshingProperty, 34 | getter: static (NewsViewModel vm) => vm.IsListRefreshing, 35 | setter: static (NewsViewModel vm, bool isRefreshing) => vm.IsListRefreshing = isRefreshing) 36 | .Bind(RefreshView.CommandProperty, 37 | getter: static (NewsViewModel vm) => vm.RefreshCommand, 38 | mode: BindingMode.OneTime); 39 | } 40 | 41 | protected override void OnAppearing() 42 | { 43 | base.OnAppearing(); 44 | 45 | if (Content is RefreshView { Content: CollectionView collectionView } refreshView 46 | && collectionView.ItemsSource.IsNullOrEmpty()) 47 | { 48 | refreshView.IsRefreshing = true; 49 | } 50 | } 51 | 52 | async void HandleSelectionChanged(object? sender, SelectionChangedEventArgs e) 53 | { 54 | ArgumentNullException.ThrowIfNull(sender); 55 | 56 | var collectionView = (CollectionView)sender; 57 | collectionView.SelectedItem = null; 58 | 59 | if (e.CurrentSelection.FirstOrDefault() is StoryModel storyModel) 60 | { 61 | if (!string.IsNullOrEmpty(storyModel.Url)) 62 | { 63 | var browserOptions = new BrowserLaunchOptions 64 | { 65 | PreferredControlColor = ColorConstants.BrowserNavigationBarTextColor, 66 | PreferredToolbarColor = ColorConstants.BrowserNavigationBarBackgroundColor 67 | }; 68 | 69 | await _browser.OpenAsync(storyModel.Url, browserOptions); 70 | } 71 | else 72 | { 73 | await DisplayAlert("Invalid Article", "ASK HN articles have no url", "OK"); 74 | } 75 | } 76 | } 77 | 78 | void HandlePullToRefreshFailed(object? sender, string message) => 79 | _dispatcher.DispatchAsync(() => DisplayAlert("Refresh Failed", message, "OK")); 80 | } -------------------------------------------------------------------------------- /sample/Platforms/Android/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/Platforms/Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Content.PM; 3 | 4 | namespace HackerNews; 5 | 6 | [Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)] 7 | public class MainActivity : MauiAppCompatActivity 8 | { 9 | } -------------------------------------------------------------------------------- /sample/Platforms/Android/MainApplication.cs: -------------------------------------------------------------------------------- 1 | using Android.App; 2 | using Android.Runtime; 3 | 4 | namespace HackerNews; 5 | 6 | [Application] 7 | public class MainApplication : MauiApplication 8 | { 9 | public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) 10 | { 11 | } 12 | 13 | protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); 14 | } -------------------------------------------------------------------------------- /sample/Platforms/Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #512BD4 4 | #d65600 5 | #2B0B98 6 | -------------------------------------------------------------------------------- /sample/Platforms/Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 14 |