├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── AzureBlobStorageSampleApp.Android ├── Assets │ └── AboutAssets.txt ├── AzureBlobStorageSampleApp.Android.csproj ├── MainActivity.cs ├── Properties │ ├── AndroidManifest.xml │ └── AssemblyInfo.cs ├── Resources │ ├── AboutResources.txt │ ├── Resources │ │ ├── values │ │ │ └── styles.xml │ │ └── xml │ │ │ └── file_paths.xml │ ├── layout │ │ ├── Tabbar.axml │ │ └── Toolbar.axml │ ├── mipmap-anydpi-v26 │ │ ├── icon.xml │ │ └── icon_round.xml │ ├── mipmap-hdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-mdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xxhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── mipmap-xxxhdpi │ │ ├── icon.png │ │ └── launcher_foreground.png │ ├── values │ │ ├── colors.xml │ │ └── styles.xml │ └── xml │ │ └── file_paths.xml └── app.config ├── AzureBlobStorageSampleApp.Functions ├── .gitignore ├── AzureBlobStorageSampleApp.Functions.csproj ├── Database │ └── PhotosDbContext.cs ├── Functions │ ├── GetPhotos.cs │ └── PostBlob.cs ├── Program.cs ├── Services │ ├── Base │ │ └── BaseBlobStorageService.cs │ ├── PhotoDatabaseService.cs │ └── PhotosBlobStorageService.cs └── host.json ├── AzureBlobStorageSampleApp.Mobile.Shared ├── AzureBlobStorageSampleApp.Mobile.Shared.projitems ├── AzureBlobStorageSampleApp.Mobile.Shared.shproj └── Constants │ ├── AutomationIdConstants.cs │ ├── BackendConstants.cs │ └── PageTitles.cs ├── AzureBlobStorageSampleApp.Shared ├── AzureBlobStorageSampleApp.Shared.projitems ├── AzureBlobStorageSampleApp.Shared.shproj └── Models │ ├── IBaseModel.cs │ └── PhotoModel.cs ├── AzureBlobStorageSampleApp.UITests ├── AppInitializer.cs ├── AzureBlobStorageSampleApp.UITests.csproj ├── Pages │ ├── AddPhotosPage.cs │ ├── BasePage.cs │ ├── PhotoDetailPage.cs │ └── PhotoListPage.cs ├── Tests │ ├── BaseTest.cs │ └── Tests.cs └── app.config ├── AzureBlobStorageSampleApp.iOS ├── AppDelegate.cs ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── AzureBlobStorageSampleApp.iOS.csproj ├── Custom Renderers │ └── EntryCustomRederer.cs ├── Entitlements.plist ├── Info.plist ├── LaunchScreen.storyboard ├── Main.cs └── app.config ├── AzureBlobStorageSampleApp.sln ├── AzureBlobStorageSampleApp ├── App.cs ├── AzureBlobStorageSampleApp.csproj ├── Constants │ └── ColorConstants.cs ├── Database │ ├── BaseDatabase.cs │ └── PhotoDatabase.cs ├── Pages │ ├── AddPhotoPage.cs │ ├── Base │ │ ├── BaseContentPage.cs │ │ └── BaseNavigationPage.cs │ ├── PhotoDetailsPage.cs │ └── PhotoListPage.cs ├── Services │ ├── APIService.cs │ ├── DatabaseSyncService.cs │ ├── DebugServices.cs │ ├── IPhotosAPI.cs │ └── RefitExtensions.cs ├── ViewModels │ ├── AddPhotoViewModel.cs │ ├── BaseViewModel.cs │ ├── PhotoDetailsViewModel.cs │ └── PhotoListViewModel.cs └── Views │ ├── MarkupExtensions.cs │ └── PhotoList │ └── PhotoDataTemplate.cs ├── Directory.Build.props ├── LICENSE.md └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [brminnick] 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | Android: 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v1 17 | 18 | - uses: actions/setup-java@v2 19 | with: 20 | distribution: 'microsoft' 21 | java-version: '17' 22 | 23 | - name: Setup .NET v6.0 24 | uses: actions/setup-dotnet@v1 25 | with: 26 | dotnet-version: '6.0.x' 27 | 28 | - name: Install Boots 29 | run : | 30 | dotnet tool install --global boots --prerelease 31 | boots --alpha Mono 32 | boots --alpha Xamarin.Android 33 | 34 | - name: Build 35 | run: | 36 | msbuild ./AzureBlobStorageSampleApp.Android/AzureBlobStorageSampleApp.Android.csproj /verbosity:normal /p:Configuration=Release /restore 37 | 38 | Functions: 39 | runs-on: macos-latest 40 | 41 | steps: 42 | - uses: actions/checkout@v1 43 | 44 | - name: Setup .NET v6.0 45 | uses: actions/setup-dotnet@v1 46 | with: 47 | dotnet-version: '6.0.x' 48 | 49 | - name: Build 50 | run: | 51 | dotnet build ./AzureBlobStorageSampleApp.Functions/AzureBlobStorageSampleApp.Functions.csproj -c Release 52 | 53 | UITests: 54 | runs-on: macos-latest 55 | 56 | steps: 57 | - uses: actions/checkout@v1 58 | 59 | - name: Setup .NET v6.0 60 | uses: actions/setup-dotnet@v1 61 | with: 62 | dotnet-version: '6.0.x' 63 | 64 | - name: Build 65 | run: | 66 | msbuild ./AzureBlobStorageSampleApp.UITests/AzureBlobStorageSampleApp.UITests.csproj /verbosity:normal /p:Configuration=Release /restore 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/xamarinstudio,visualstudio,visualstudiocode,xcode,android,macos,csharp,f#,fastlane,java,jetbrains,linux,monodevelop,objective-c,swift,sublimetext,unity 3 | 4 | ### fastlane ### 5 | # fastlane - A streamlined workflow tool for Cocoa deployment 6 | 7 | # fastlane specific 8 | fastlane/report.xml 9 | 10 | # deliver temporary files 11 | fastlane/Preview.html 12 | 13 | # snapshot generated screenshots 14 | fastlane/screenshots/**/*.png 15 | fastlane/screenshots/screenshots.html 16 | 17 | # scan temporary files 18 | fastlane/test_output 19 | 20 | 21 | ### XamarinStudio ### 22 | bin/ 23 | obj/ 24 | *.userprefs 25 | 26 | 27 | ### VisualStudioCode ### 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | 34 | 35 | ### Xcode ### 36 | # Xcode 37 | # 38 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 39 | 40 | ## Build generated 41 | build/ 42 | DerivedData/ 43 | 44 | ## Various settings 45 | *.pbxuser 46 | !default.pbxuser 47 | *.mode1v3 48 | !default.mode1v3 49 | *.mode2v3 50 | !default.mode2v3 51 | *.perspectivev3 52 | !default.perspectivev3 53 | xcuserdata/ 54 | 55 | ## Other 56 | *.moved-aside 57 | *.xccheckout 58 | *.xcscmblueprint 59 | 60 | 61 | ### Android ### 62 | # Built application files 63 | *.apk 64 | *.ap_ 65 | 66 | # Files for the ART/Dalvik VM 67 | *.dex 68 | 69 | # Java class files 70 | *.class 71 | 72 | # Generated files 73 | gen/ 74 | out/ 75 | Resource.designer.cs 76 | 77 | # Gradle files 78 | .gradle/ 79 | 80 | # Local configuration file (sdk path, etc) 81 | local.properties 82 | 83 | # Proguard folder generated by Eclipse 84 | proguard/ 85 | 86 | # Log Files 87 | *.log 88 | 89 | # Android Studio Navigation editor temp files 90 | .navigation/ 91 | 92 | # Android Studio captures folder 93 | captures/ 94 | 95 | # Intellij 96 | *.iml 97 | .idea/workspace.xml 98 | .idea/tasks.xml 99 | .idea/libraries 100 | 101 | # Keystore files 102 | *.jks 103 | 104 | # External native build folder generated in Android Studio 2.2 and later 105 | .externalNativeBuild 106 | 107 | ### Android Patch ### 108 | gen-external-apklibs 109 | 110 | 111 | ### macOS ### 112 | *.DS_Store 113 | .AppleDouble 114 | .LSOverride 115 | 116 | # Icon must end with two \r 117 | Icon 118 | # Thumbnails 119 | ._* 120 | # Files that might appear in the root of a volume 121 | .DocumentRevisions-V100 122 | .fseventsd 123 | .Spotlight-V100 124 | .TemporaryItems 125 | .Trashes 126 | .VolumeIcon.icns 127 | .com.apple.timemachine.donotpresent 128 | # Directories potentially created on remote AFP share 129 | .AppleDB 130 | .AppleDesktop 131 | Network Trash Folder 132 | Temporary Items 133 | .apdisk 134 | 135 | 136 | ### Csharp ### 137 | ## Ignore Visual Studio temporary files, build results, and 138 | ## files generated by popular Visual Studio add-ons. 139 | ## 140 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 141 | 142 | # User-specific files 143 | *.suo 144 | *.user 145 | *.userosscache 146 | *.sln.docstates 147 | *.vcxproj.filters 148 | 149 | # User-specific files (MonoDevelop/Xamarin Studio) 150 | 151 | # Build results 152 | [Dd]ebug/ 153 | [Dd]ebugPublic/ 154 | [Rr]elease/ 155 | [Rr]eleases/ 156 | x64/ 157 | x86/ 158 | bld/ 159 | [Bb]in/ 160 | [Oo]bj/ 161 | [Ll]og/ 162 | 163 | # Visual Studio 2015 cache/options directory 164 | .vs/ 165 | # Uncomment if you have tasks that create the project's static files in wwwroot 166 | #wwwroot/ 167 | 168 | # MSTest test Results 169 | [Tt]est[Rr]esult*/ 170 | [Bb]uild[Ll]og.* 171 | 172 | # NUNIT 173 | *.VisualState.xml 174 | TestResult.xml 175 | 176 | # Build Results of an ATL Project 177 | [Dd]ebugPS/ 178 | [Rr]eleasePS/ 179 | dlldata.c 180 | 181 | # .NET Core 182 | project.lock.json 183 | project.fragment.lock.json 184 | artifacts/ 185 | **/Properties/launchSettings.json 186 | 187 | *_i.c 188 | *_p.c 189 | *_i.h 190 | *.ilk 191 | *.meta 192 | *.obj 193 | *.pch 194 | *.pdb 195 | *.pgc 196 | *.pgd 197 | *.rsp 198 | *.sbr 199 | *.tlb 200 | *.tli 201 | *.tlh 202 | *.tmp 203 | *.tmp_proj 204 | *.vspscc 205 | *.vssscc 206 | .builds 207 | *.pidb 208 | *.svclog 209 | *.scc 210 | 211 | # Chutzpah Test files 212 | _Chutzpah* 213 | 214 | # Visual C++ cache files 215 | ipch/ 216 | *.aps 217 | *.ncb 218 | *.opendb 219 | *.opensdf 220 | *.sdf 221 | *.cachefile 222 | *.VC.db 223 | *.VC.VC.opendb 224 | 225 | # Visual Studio profiler 226 | *.psess 227 | *.vsp 228 | *.vspx 229 | *.sap 230 | 231 | # TFS 2012 Local Workspace 232 | $tf/ 233 | 234 | # Guidance Automation Toolkit 235 | *.gpState 236 | 237 | # ReSharper is a .NET coding add-in 238 | _ReSharper*/ 239 | *.[Rr]e[Ss]harper 240 | *.DotSettings.user 241 | 242 | # JustCode is a .NET coding add-in 243 | .JustCode 244 | 245 | # TeamCity is a build add-in 246 | _TeamCity* 247 | 248 | # DotCover is a Code Coverage Tool 249 | *.dotCover 250 | 251 | # Visual Studio code coverage results 252 | *.coverage 253 | *.coveragexml 254 | 255 | # NCrunch 256 | _NCrunch_* 257 | .*crunch*.local.xml 258 | nCrunchTemp_* 259 | 260 | # MightyMoose 261 | *.mm.* 262 | AutoTest.Net/ 263 | 264 | # Web workbench (sass) 265 | .sass-cache/ 266 | 267 | # Installshield output folder 268 | [Ee]xpress/ 269 | 270 | # DocProject is a documentation generator add-in 271 | DocProject/buildhelp/ 272 | DocProject/Help/*.HxT 273 | DocProject/Help/*.HxC 274 | DocProject/Help/*.hhc 275 | DocProject/Help/*.hhk 276 | DocProject/Help/*.hhp 277 | DocProject/Help/Html2 278 | DocProject/Help/html 279 | 280 | # Click-Once directory 281 | publish/ 282 | 283 | # Publish Web Output 284 | *.[Pp]ublish.xml 285 | *.azurePubxml 286 | # TODO: Comment the next line if you want to checkin your web deploy settings 287 | # but database connection strings (with potential passwords) will be unencrypted 288 | *.pubxml 289 | *.publishproj 290 | 291 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 292 | # checkin your Azure Web App publish settings, but sensitive information contained 293 | # in these scripts will be unencrypted 294 | PublishScripts/ 295 | 296 | # NuGet Packages 297 | *.nupkg 298 | # The packages folder can be ignored because of Package Restore 299 | **/packages/* 300 | # except build/, which is used as an MSBuild target. 301 | !**/packages/build/ 302 | # Uncomment if necessary however generally it will be regenerated when needed 303 | #!**/packages/repositories.config 304 | # NuGet v3's project.json files produces more ignoreable files 305 | *.nuget.props 306 | *.nuget.targets 307 | 308 | # Microsoft Azure Build Output 309 | csx/ 310 | *.build.csdef 311 | 312 | # Microsoft Azure Emulator 313 | ecf/ 314 | rcf/ 315 | 316 | # Windows Store app package directories and files 317 | AppPackages/ 318 | BundleArtifacts/ 319 | Package.StoreAssociation.xml 320 | _pkginfo.txt 321 | 322 | # Visual Studio cache files 323 | # files ending in .cache can be ignored 324 | *.[Cc]ache 325 | # but keep track of directories ending in .cache 326 | !*.[Cc]ache/ 327 | 328 | # Others 329 | ClientBin/ 330 | ~$* 331 | *~ 332 | *.dbmdl 333 | *.dbproj.schemaview 334 | *.jfm 335 | *.pfx 336 | *.publishsettings 337 | node_modules/ 338 | orleans.codegen.cs 339 | 340 | # Since there are multiple workflows, uncomment next line to ignore bower_components 341 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 342 | #bower_components/ 343 | 344 | # RIA/Silverlight projects 345 | Generated_Code/ 346 | 347 | # Backup & report files from converting an old project file 348 | # to a newer Visual Studio version. Backup files are not needed, 349 | # because we have git ;-) 350 | _UpgradeReport_Files/ 351 | Backup*/ 352 | UpgradeLog*.XML 353 | UpgradeLog*.htm 354 | 355 | # SQL Server files 356 | *.mdf 357 | *.ldf 358 | 359 | # Business Intelligence projects 360 | *.rdl.data 361 | *.bim.layout 362 | *.bim_*.settings 363 | 364 | # Microsoft Fakes 365 | FakesAssemblies/ 366 | 367 | # GhostDoc plugin setting file 368 | *.GhostDoc.xml 369 | 370 | # Node.js Tools for Visual Studio 371 | .ntvs_analysis.dat 372 | 373 | # Visual Studio 6 build log 374 | *.plg 375 | 376 | # Visual Studio 6 workspace options file 377 | *.opt 378 | 379 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 380 | *.vbw 381 | 382 | # Visual Studio LightSwitch build output 383 | **/*.HTMLClient/GeneratedArtifacts 384 | **/*.DesktopClient/GeneratedArtifacts 385 | **/*.DesktopClient/ModelManifest.xml 386 | **/*.Server/GeneratedArtifacts 387 | **/*.Server/ModelManifest.xml 388 | _Pvt_Extensions 389 | 390 | # Paket dependency manager 391 | .paket/paket.exe 392 | paket-files/ 393 | 394 | # FAKE - F# Make 395 | .fake/ 396 | 397 | # JetBrains Rider 398 | .idea/ 399 | *.sln.iml 400 | 401 | # CodeRush 402 | .cr/ 403 | 404 | # Python Tools for Visual Studio (PTVS) 405 | __pycache__/ 406 | *.pyc 407 | 408 | # Cake - Uncomment if you are using it 409 | # tools/ 410 | 411 | 412 | ### F# ### 413 | lib/debug 414 | lib/release 415 | Debug 416 | obj 417 | bin 418 | *.exe 419 | !.paket/paket.bootstrapper.exe 420 | 421 | 422 | ### SublimeText ### 423 | # cache files for sublime text 424 | *.tmlanguage.cache 425 | *.tmPreferences.cache 426 | *.stTheme.cache 427 | 428 | # workspace files are user-specific 429 | *.sublime-workspace 430 | 431 | # project files should be checked into the repository, unless a significant 432 | # proportion of contributors will probably not be using SublimeText 433 | # *.sublime-project 434 | 435 | # sftp configuration file 436 | sftp-config.json 437 | 438 | # Package control specific files 439 | Package Control.last-run 440 | Package Control.ca-list 441 | Package Control.ca-bundle 442 | Package Control.system-ca-bundle 443 | Package Control.cache/ 444 | Package Control.ca-certs/ 445 | bh_unicode_properties.cache 446 | 447 | # Sublime-github package stores a github token in this file 448 | # https://packagecontrol.io/packages/sublime-github 449 | GitHub.sublime-settings 450 | 451 | 452 | ### Swift ### 453 | # Xcode 454 | # 455 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 456 | 457 | ## Build generated 458 | 459 | ## Various settings 460 | 461 | ## Other 462 | *.xcuserstate 463 | 464 | ## Obj-C/Swift specific 465 | *.hmap 466 | *.ipa 467 | *.dSYM.zip 468 | *.dSYM 469 | 470 | ## Playgrounds 471 | timeline.xctimeline 472 | playground.xcworkspace 473 | 474 | # Swift Package Manager 475 | # 476 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 477 | # Packages/ 478 | .build/ 479 | 480 | # CocoaPods 481 | # 482 | # We recommend against adding the Pods directory to your .gitignore. However 483 | # you should judge for yourself, the pros and cons are mentioned at: 484 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 485 | # 486 | # Pods/ 487 | 488 | # Carthage 489 | # 490 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 491 | # Carthage/Checkouts 492 | 493 | Carthage/Build 494 | 495 | # fastlane 496 | # 497 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 498 | # screenshots whenever they are needed. 499 | # For more information about the recommended setup visit: 500 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 501 | 502 | fastlane/screenshots 503 | 504 | 505 | ### JetBrains ### 506 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 507 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 508 | 509 | # User-specific stuff: 510 | 511 | # Sensitive or high-churn files: 512 | .idea/dataSources/ 513 | .idea/dataSources.ids 514 | .idea/dataSources.xml 515 | .idea/dataSources.local.xml 516 | .idea/sqlDataSources.xml 517 | .idea/dynamic.xml 518 | .idea/uiDesigner.xml 519 | 520 | # Gradle: 521 | .idea/gradle.xml 522 | 523 | # Mongo Explorer plugin: 524 | .idea/mongoSettings.xml 525 | 526 | ## File-based project format: 527 | *.iws 528 | 529 | ## Plugin-specific files: 530 | 531 | # IntelliJ 532 | /out/ 533 | 534 | # mpeltonen/sbt-idea plugin 535 | .idea_modules/ 536 | 537 | # JIRA plugin 538 | atlassian-ide-plugin.xml 539 | 540 | # Crashlytics plugin (for Android Studio and IntelliJ) 541 | com_crashlytics_export_strings.xml 542 | crashlytics.properties 543 | crashlytics-build.properties 544 | fabric.properties 545 | 546 | ### JetBrains Patch ### 547 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 548 | 549 | # *.iml 550 | # modules.xml 551 | # .idea/misc.xml 552 | # *.ipr 553 | 554 | 555 | ### Linux ### 556 | 557 | # temporary files which can be created if a process still has a handle open of a deleted file 558 | .fuse_hidden* 559 | 560 | # KDE directory preferences 561 | .directory 562 | 563 | # Linux trash folder which might appear on any partition or disk 564 | .Trash-* 565 | 566 | # .nfs files are created when an open file is removed but is still being accessed 567 | .nfs* 568 | 569 | 570 | ### MonoDevelop ### 571 | #User Specific 572 | *.usertasks 573 | 574 | #Mono Project Files 575 | *.resources 576 | test-results/ 577 | 578 | 579 | ### Objective-C ### 580 | # Xcode 581 | # 582 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 583 | 584 | ## Build generated 585 | 586 | ## Various settings 587 | 588 | ## Other 589 | 590 | ## Obj-C/Swift specific 591 | 592 | # CocoaPods 593 | # 594 | # We recommend against adding the Pods directory to your .gitignore. However 595 | # you should judge for yourself, the pros and cons are mentioned at: 596 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 597 | # 598 | # Pods/ 599 | 600 | # Carthage 601 | # 602 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 603 | # Carthage/Checkouts 604 | 605 | 606 | # fastlane 607 | # 608 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 609 | # screenshots whenever they are needed. 610 | # For more information about the recommended setup visit: 611 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 612 | 613 | 614 | # Code Injection 615 | # 616 | # After new code Injection tools there's a generated folder /iOSInjectionProject 617 | # https://github.com/johnno1962/injectionforxcode 618 | 619 | iOSInjectionProject/ 620 | 621 | ### Objective-C Patch ### 622 | 623 | 624 | ### Unity ### 625 | /[Ll]ibrary/ 626 | /[Tt]emp/ 627 | /[Oo]bj/ 628 | /[Bb]uild/ 629 | /[Bb]uilds/ 630 | /Assets/AssetStoreTools* 631 | 632 | # Autogenerated VS/MD/Consulo solution and project files 633 | ExportedObj/ 634 | .consulo/*.csproj 635 | .consulo/*.unityproj 636 | .consulo/*.sln 637 | .consulo/*.booproj 638 | .consulo/*.svd 639 | 640 | 641 | # Unity3D generated meta files 642 | *.pidb.meta 643 | 644 | # Unity3D Generated File On Crash Reports 645 | sysinfo.txt 646 | 647 | # Builds 648 | *.unitypackage 649 | 650 | 651 | ### Java ### 652 | 653 | # BlueJ files 654 | *.ctxt 655 | 656 | # Mobile Tools for Java (J2ME) 657 | .mtj.tmp/ 658 | 659 | # Package Files # 660 | *.jar 661 | *.war 662 | *.ear 663 | 664 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 665 | hs_err_pid* 666 | 667 | 668 | ### VisualStudio ### 669 | ## Ignore Visual Studio temporary files, build results, and 670 | ## files generated by popular Visual Studio add-ons. 671 | ## 672 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 673 | 674 | # User-specific files 675 | 676 | # User-specific files (MonoDevelop/Xamarin Studio) 677 | 678 | # Build results 679 | 680 | # Visual Studio 2015 cache/options directory 681 | # Uncomment if you have tasks that create the project's static files in wwwroot 682 | #wwwroot/ 683 | 684 | # MSTest test Results 685 | 686 | # NUNIT 687 | 688 | # Build Results of an ATL Project 689 | 690 | # .NET Core 691 | 692 | 693 | # Chutzpah Test files 694 | 695 | # Visual C++ cache files 696 | 697 | # Visual Studio profiler 698 | 699 | # TFS 2012 Local Workspace 700 | 701 | # Guidance Automation Toolkit 702 | 703 | # ReSharper is a .NET coding add-in 704 | 705 | # JustCode is a .NET coding add-in 706 | 707 | # TeamCity is a build add-in 708 | 709 | # DotCover is a Code Coverage Tool 710 | 711 | # Visual Studio code coverage results 712 | 713 | # NCrunch 714 | 715 | # MightyMoose 716 | 717 | # Web workbench (sass) 718 | 719 | # Installshield output folder 720 | 721 | # DocProject is a documentation generator add-in 722 | 723 | # Click-Once directory 724 | 725 | # Publish Web Output 726 | # TODO: Comment the next line if you want to checkin your web deploy settings 727 | # but database connection strings (with potential passwords) will be unencrypted 728 | 729 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 730 | # checkin your Azure Web App publish settings, but sensitive information contained 731 | # in these scripts will be unencrypted 732 | 733 | # NuGet Packages 734 | # The packages folder can be ignored because of Package Restore 735 | # except build/, which is used as an MSBuild target. 736 | # Uncomment if necessary however generally it will be regenerated when needed 737 | #!**/packages/repositories.config 738 | # NuGet v3's project.json files produces more ignoreable files 739 | 740 | # Microsoft Azure Build Output 741 | 742 | # Microsoft Azure Emulator 743 | 744 | # Windows Store app package directories and files 745 | 746 | # Visual Studio cache files 747 | # files ending in .cache can be ignored 748 | # but keep track of directories ending in .cache 749 | 750 | # Others 751 | 752 | # Since there are multiple workflows, uncomment next line to ignore bower_components 753 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 754 | #bower_components/ 755 | 756 | # RIA/Silverlight projects 757 | 758 | # Backup & report files from converting an old project file 759 | # to a newer Visual Studio version. Backup files are not needed, 760 | # because we have git ;-) 761 | 762 | # SQL Server files 763 | 764 | # Business Intelligence projects 765 | 766 | # Microsoft Fakes 767 | 768 | # GhostDoc plugin setting file 769 | 770 | # Node.js Tools for Visual Studio 771 | 772 | # Visual Studio 6 build log 773 | 774 | # Visual Studio 6 workspace options file 775 | 776 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 777 | 778 | # Visual Studio LightSwitch build output 779 | 780 | # Paket dependency manager 781 | 782 | # FAKE - F# Make 783 | 784 | # JetBrains Rider 785 | 786 | # CodeRush 787 | 788 | # Python Tools for Visual Studio (PTVS) 789 | 790 | # Cake - Uncomment if you are using it 791 | # tools/ 792 | 793 | ### VisualStudio Patch ### 794 | 795 | # End of https://www.gitignore.io/api/xamarinstudio,visualstudio,visualstudiocode,xcode,android,macos,csharp,f#,fastlane,java,jetbrains,linux,monodevelop,objective-c,swift,sublimetext,unity 796 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Assets/AboutAssets.txt: -------------------------------------------------------------------------------- 1 | Any raw assets you want to be deployed with your application can be placed in 2 | this directory (and child directories) and given a Build Action of "AndroidAsset". 3 | 4 | These files will be deployed with you package and will be accessible using Android's 5 | AssetManager, like this: 6 | 7 | public class ReadAsset : Activity 8 | { 9 | protected override void OnCreate (Bundle bundle) 10 | { 11 | base.OnCreate (bundle); 12 | 13 | InputStream input = Assets.Open ("my_asset.txt"); 14 | } 15 | } 16 | 17 | Additionally, some Android functions will automatically load asset files: 18 | 19 | Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); 20 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/AzureBlobStorageSampleApp.Android.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {9AA352A4-133F-4D31-8517-91B11010DD0C} 7 | {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 8 | {c9e5eea5-ca05-42a1-839b-61506e0a37df} 9 | Library 10 | AzureBlobStorageSampleApp.Android 11 | AzureBlobStorageSampleApp.Android 12 | True 13 | Resources\Resource.designer.cs 14 | Resource 15 | Properties\AndroidManifest.xml 16 | Resources 17 | Assets 18 | v12.0 19 | Xamarin.Android.Net.AndroidClientHandler 20 | 21 | 22 | false 23 | 24 | 25 | true 26 | portable 27 | false 28 | bin\Debug 29 | DEBUG; 30 | prompt 31 | 4 32 | None 33 | true 34 | armeabi-v7a;x86;arm64-v8a;x86_64 35 | AndroidClientHandler 36 | Default (Native TLS 1.2+) 37 | 38 | 39 | 40 | true 41 | pdbonly 42 | true 43 | bin\Release 44 | prompt 45 | 4 46 | true 47 | true 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B} 109 | AzureBlobStorageSampleApp 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/MainActivity.cs: -------------------------------------------------------------------------------- 1 | using Android.OS; 2 | using Android.App; 3 | using Android.Runtime; 4 | using Android.Content.PM; 5 | 6 | namespace AzureBlobStorageSampleApp.Android 7 | { 8 | [Activity(Label = "AzureBlobStorageSampleApp.Android", Icon = "@mipmap/icon", Theme = "@style/MyTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)] 9 | public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity 10 | { 11 | public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults) 12 | { 13 | Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults); 14 | 15 | base.OnRequestPermissionsResult(requestCode, permissions, grantResults); 16 | } 17 | 18 | protected override void OnCreate(Bundle savedInstanceState) 19 | { 20 | TabLayoutResource = Resource.Layout.Tabbar; 21 | ToolbarResource = Resource.Layout.Toolbar; 22 | 23 | base.OnCreate(savedInstanceState); 24 | 25 | Xamarin.Essentials.Platform.Init(this, savedInstanceState); 26 | global::Xamarin.Forms.Forms.Init(this, savedInstanceState); 27 | FFImageLoading.Forms.Platform.CachedImageRenderer.Init(true); 28 | FFImageLoading.Forms.Platform.CachedImageRenderer.InitImageViewHandler(); 29 | 30 | LoadApplication(new App()); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Properties/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | using Android.App; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("AzureBlobStorageSampleApp.Android")] 10 | [assembly: AssemblyDescription("")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("")] 13 | [assembly: AssemblyProduct("AzureBlobStorageSampleApp.Android")] 14 | [assembly: AssemblyCopyright("Copyright © 2014")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | [assembly: ComVisible(false)] 18 | 19 | // Version information for an assembly consists of the following four values: 20 | // 21 | // Major Version 22 | // Minor Version 23 | // Build Number 24 | // Revision 25 | // 26 | // You can specify all the values or you can default the Build and Revision Numbers 27 | // by using the '*' as shown below: 28 | // [assembly: AssemblyVersion("1.0.*")] 29 | [assembly: AssemblyVersion("1.0.0.0")] 30 | [assembly: AssemblyFileVersion("1.0.0.0")] 31 | 32 | // Add some common permissions, these can be removed if not needed 33 | [assembly: UsesPermission(Android.Manifest.Permission.Internet)] 34 | [assembly: UsesPermission(Android.Manifest.Permission.WriteExternalStorage)] 35 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/AboutResources.txt: -------------------------------------------------------------------------------- 1 | Images, layout descriptions, binary blobs and string dictionaries can be included 2 | in your application as resource files. Various Android APIs are designed to 3 | operate on the resource IDs instead of dealing with images, strings or binary blobs 4 | directly. 5 | 6 | For example, a sample Android app that contains a user interface layout (main.xml), 7 | an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) 8 | would keep its resources in the "Resources" directory of the application: 9 | 10 | Resources/ 11 | drawable-hdpi/ 12 | icon.png 13 | 14 | drawable-ldpi/ 15 | icon.png 16 | 17 | drawable-mdpi/ 18 | icon.png 19 | 20 | layout/ 21 | main.xml 22 | 23 | values/ 24 | strings.xml 25 | 26 | In order to get the build system to recognize Android resources, set the build action to 27 | "AndroidResource". The native Android APIs do not operate directly with filenames, but 28 | instead operate on resource IDs. When you compile an Android application that uses resources, 29 | the build system will package the resources for distribution and generate a class called 30 | "Resource" that contains the tokens for each one of the resources included. For example, 31 | for the above Resources layout, this is what the Resource class would expose: 32 | 33 | public class Resource { 34 | public class drawable { 35 | public const int icon = 0x123; 36 | } 37 | 38 | public class layout { 39 | public const int main = 0x456; 40 | } 41 | 42 | public class strings { 43 | public const int first_string = 0xabc; 44 | public const int second_string = 0xbcd; 45 | } 46 | } 47 | 48 | You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main 49 | to reference the layout/main.xml file, or Resource.strings.first_string to reference the first 50 | string in the dictionary file values/strings.xml. 51 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 5 | 6 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/Resources/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/layout/Tabbar.axml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/layout/Toolbar.axml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-anydpi-v26/icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-anydpi-v26/icon_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-hdpi/icon.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-hdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-hdpi/launcher_foreground.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-mdpi/icon.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-mdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-mdpi/launcher_foreground.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-xhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-xhdpi/icon.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-xhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-xhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-xxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-xxhdpi/icon.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-xxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-xxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-xxxhdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-xxxhdpi/icon.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/mipmap-xxxhdpi/launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TheCodeTraveler/AzureBlobStorageSampleApp/a8417acee88fdf15b369d7d0f30c34b4da4ee5ff/AzureBlobStorageSampleApp.Android/Resources/mipmap-xxxhdpi/launcher_foreground.png -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | #3F51B5 5 | #303F9F 6 | #FF4081 7 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/values/styles.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 5 | 6 | 24 | 27 | 28 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/Resources/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Android/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/AzureBlobStorageSampleApp.Functions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | v4 6 | Exe 7 | 8 | 9 | TRACE;DEBUG;NETSTANDARD;NETSTANDARD2_0;BACKEND 10 | 11 | 12 | 13 | 14 | TRACE;RELEASE;NETSTANDARD;NETSTANDARD2_0;BACKEND 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/Database/PhotosDbContext.cs: -------------------------------------------------------------------------------- 1 | using AzureBlobStorageSampleApp.Shared; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace AzureBlobStorageSampleApp.Functions 5 | { 6 | class PhotosDbContext : DbContext 7 | { 8 | public PhotosDbContext(DbContextOptions options) : base(options) 9 | { 10 | Database.EnsureCreated(); 11 | } 12 | 13 | public DbSet Photos => Set(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/Functions/GetPhotos.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using Microsoft.Azure.Functions.Worker; 6 | using Microsoft.Azure.Functions.Worker.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace AzureBlobStorageSampleApp.Functions 10 | { 11 | class GetPhotos 12 | { 13 | readonly PhotoDatabaseService _photoDatabaseService; 14 | 15 | public GetPhotos(PhotoDatabaseService photoDatabaseService) => _photoDatabaseService = photoDatabaseService; 16 | 17 | [Function(nameof(GetPhotos))] 18 | public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req, FunctionContext context) 19 | { 20 | var log = context.GetLogger(); 21 | log.LogInformation("C# HTTP trigger function processed a request."); 22 | 23 | try 24 | { 25 | var photoList = await _photoDatabaseService.GetAllPhotos().ConfigureAwait(false); 26 | 27 | var okResponse = req.CreateResponse(System.Net.HttpStatusCode.OK); 28 | await okResponse.WriteAsJsonAsync(photoList); 29 | 30 | return okResponse; 31 | } 32 | catch (Exception e) 33 | { 34 | log.LogError(e, e.Message); 35 | 36 | var errorResponse = req.CreateResponse(System.Net.HttpStatusCode.InternalServerError); 37 | return errorResponse; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/Functions/PostBlob.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using HttpMultipartParser; 5 | using Microsoft.Azure.Functions.Worker; 6 | using Microsoft.Azure.Functions.Worker.Http; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace AzureBlobStorageSampleApp.Functions 10 | { 11 | class PostBlob 12 | { 13 | readonly PhotosBlobStorageService _photosBlobStorageService; 14 | readonly PhotoDatabaseService _photoDatabaseService; 15 | 16 | public PostBlob(PhotosBlobStorageService photosBlobStorageService, PhotoDatabaseService photoDatabaseService) => 17 | (_photosBlobStorageService, _photoDatabaseService) = (photosBlobStorageService, photoDatabaseService); 18 | 19 | [Function(nameof(PostBlob))] 20 | public async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "PostBlob/{title}")]HttpRequestData req, string title, FunctionContext context) 21 | { 22 | var log = context.GetLogger(); 23 | 24 | try 25 | { 26 | var multipartFormParser = await MultipartFormDataParser.ParseAsync(req.Body); 27 | var photoStream = multipartFormParser.Files[0].Data; 28 | 29 | var photo = await _photosBlobStorageService.SavePhoto(photoStream, title).ConfigureAwait(false); 30 | log.LogInformation("Saved Photo to Blob Storage"); 31 | 32 | await _photoDatabaseService.InsertPhoto(photo).ConfigureAwait(false); 33 | log.LogInformation("Saved Photo to Database"); 34 | 35 | var response = req.CreateResponse(HttpStatusCode.Created); 36 | await response.WriteAsJsonAsync(photo).ConfigureAwait(false); 37 | 38 | return response; 39 | } 40 | catch (Exception e) 41 | { 42 | log.LogError(e, e.Message); 43 | 44 | var response = req.CreateResponse(HttpStatusCode.InternalServerError); 45 | return response; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Azure.Storage.Blobs; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | 9 | namespace AzureBlobStorageSampleApp.Functions 10 | { 11 | class Program 12 | { 13 | readonly static string _connectionString = Environment.GetEnvironmentVariable("PhotoDatabaseConnectionString") ?? string.Empty; 14 | readonly static string _storageConnectionString = Environment.GetEnvironmentVariable("AzureWebJobsStorage") ?? string.Empty; 15 | 16 | static Task Main(string[] args) 17 | { 18 | var host = new HostBuilder() 19 | .ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddCommandLine(args)) 20 | .ConfigureFunctionsWorkerDefaults() 21 | .ConfigureServices(services => 22 | { 23 | services.AddLogging(); 24 | 25 | services.AddDbContext(options => options.UseSqlServer(_connectionString)); 26 | 27 | services.AddTransient(); 28 | services.AddTransient(); 29 | services.AddTransient(_ => new BlobServiceClient(_storageConnectionString)); 30 | }).Build(); 31 | 32 | return host.RunAsync(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/Services/Base/BaseBlobStorageService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Azure.Storage.Blobs; 7 | using Azure.Storage.Blobs.Models; 8 | using Newtonsoft.Json; 9 | 10 | namespace AzureBlobStorageSampleApp.Functions 11 | { 12 | abstract class BaseBlobStorageService 13 | { 14 | readonly BlobServiceClient _blobServiceClient; 15 | 16 | protected BaseBlobStorageService(BlobServiceClient cloudBlobClient) => _blobServiceClient = cloudBlobClient; 17 | 18 | protected async Task> UploadStream(Stream data, string blobName, string containerName) 19 | { 20 | var containerClient = GetBlobContainerClient(containerName); 21 | await containerClient.CreateIfNotExistsAsync().ConfigureAwait(false); 22 | 23 | var blobClient = containerClient.GetBlobClient(blobName); 24 | 25 | return await blobClient.UploadAsync(data).ConfigureAwait(false); 26 | } 27 | 28 | protected async Task GetLatestValue(string containerName) 29 | { 30 | var blobList = new List(); 31 | await foreach (var blob in GetBlobs(containerName).ConfigureAwait(false)) 32 | { 33 | blobList.Add(blob); 34 | } 35 | 36 | var newestBlob = blobList.OrderByDescending(x => x.Properties.CreatedOn).First(); 37 | 38 | var blobClient = GetBlobContainerClient(containerName).GetBlobClient(newestBlob.Name); 39 | var blobContentResponse = await blobClient.DownloadContentAsync().ConfigureAwait(false); 40 | 41 | var serializedBlobContents = blobContentResponse.Value.Content; 42 | 43 | return JsonConvert.DeserializeObject(serializedBlobContents.ToString()) ?? throw new NullReferenceException(); 44 | } 45 | 46 | protected async IAsyncEnumerable GetBlobs(string containerName) 47 | { 48 | var blobContainerClient = GetBlobContainerClient(containerName); 49 | 50 | await foreach (var blob in blobContainerClient.GetBlobsAsync().ConfigureAwait(false)) 51 | { 52 | if (blob is not null) 53 | yield return blob; 54 | } 55 | } 56 | 57 | protected BlobContainerClient GetBlobContainerClient(string containerName) => _blobServiceClient.GetBlobContainerClient(containerName); 58 | } 59 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/Services/PhotoDatabaseService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using AzureBlobStorageSampleApp.Shared; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace AzureBlobStorageSampleApp.Functions 9 | { 10 | class PhotoDatabaseService 11 | { 12 | readonly PhotosDbContext _photosDbContext; 13 | 14 | public PhotoDatabaseService(PhotosDbContext photosDbContext) => _photosDbContext = photosDbContext; 15 | 16 | public async Task> GetAllPhotos() => 17 | await _photosDbContext.Photos.ToListAsync().ConfigureAwait(false); 18 | 19 | public IReadOnlyList GetAllPhotos(Func wherePredicate) => 20 | _photosDbContext.Photos.Where(wherePredicate).ToList(); 21 | 22 | public async Task InsertPhoto(PhotoModel photo) 23 | { 24 | if (string.IsNullOrWhiteSpace(photo.Id)) 25 | photo = photo with { Id = Guid.NewGuid().ToString() }; 26 | 27 | var currentTime = DateTimeOffset.UtcNow; 28 | 29 | photo = photo with 30 | { 31 | CreatedAt = currentTime, 32 | UpdatedAt = currentTime 33 | }; 34 | 35 | await _photosDbContext.AddAsync(photo).ConfigureAwait(false); 36 | await _photosDbContext.SaveChangesAsync().ConfigureAwait(false); 37 | 38 | return photo; 39 | } 40 | 41 | public async Task UpdatePhoto(PhotoModel photo) 42 | { 43 | var photoFromDatabase = _photosDbContext.Photos.Single(y => y.Id.Equals(photo.Id)); 44 | 45 | var updatedPhoto = photoFromDatabase with 46 | { 47 | IsDeleted = photo.IsDeleted, 48 | Title = photo.Title, 49 | Url = photo.Url, 50 | UpdatedAt = DateTimeOffset.UtcNow 51 | }; 52 | 53 | _photosDbContext.Update(updatedPhoto); 54 | await _photosDbContext.SaveChangesAsync().ConfigureAwait(false); 55 | 56 | return photoFromDatabase; 57 | } 58 | 59 | public async Task DeletePhoto(string id) 60 | { 61 | var photoFromDatabase = _photosDbContext.Photos.Single(x => x.Id.Equals(id)); 62 | 63 | _photosDbContext.Remove(photoFromDatabase); 64 | await _photosDbContext.SaveChangesAsync().ConfigureAwait(false); 65 | 66 | return photoFromDatabase; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/Services/PhotosBlobStorageService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Threading.Tasks; 5 | using Azure.Storage.Blobs; 6 | using AzureBlobStorageSampleApp.Shared; 7 | 8 | namespace AzureBlobStorageSampleApp.Functions 9 | { 10 | class PhotosBlobStorageService : BaseBlobStorageService 11 | { 12 | readonly string _photosContainerName = Environment.GetEnvironmentVariable("PhotoContainerName") ?? string.Empty; 13 | 14 | protected PhotosBlobStorageService(BlobServiceClient cloudBlobClient) : base(cloudBlobClient) 15 | { 16 | 17 | } 18 | 19 | public async Task SavePhoto(Stream photoStream, string photoTitle) 20 | { 21 | var containerClient = GetBlobContainerClient(_photosContainerName); 22 | 23 | await UploadStream(photoStream, photoTitle, _photosContainerName).ConfigureAwait(false); 24 | 25 | return new PhotoModel { Title = photoTitle, Url = $"{containerClient.Uri}\\{photoTitle}" }; 26 | } 27 | 28 | public async IAsyncEnumerable GetAllPhotos() 29 | { 30 | var containerClient = GetBlobContainerClient(_photosContainerName); 31 | 32 | await foreach (var blob in GetBlobs(_photosContainerName).ConfigureAwait(false)) 33 | { 34 | yield return new PhotoModel { Url = $"{containerClient.Uri}\\{blob.Name}" }; 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Functions/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Mobile.Shared/AzureBlobStorageSampleApp.Mobile.Shared.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | {FDA2EE68-4202-4C8A-BC75-6B41E95EA3CC} 7 | 8 | 9 | AzureBlobStorageSampleApp.Mobile.Shared 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Mobile.Shared/AzureBlobStorageSampleApp.Mobile.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {FDA2EE68-4202-4C8A-BC75-6B41E95EA3CC} 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Mobile.Shared/Constants/AutomationIdConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureBlobStorageSampleApp.Mobile.Shared 2 | { 3 | public static class AutomationIdConstants 4 | { 5 | #region Photo List Page 6 | public const string AddPhotoButton = nameof(AddPhotoButton); 7 | public const string PhotosCollectionView = nameof(PhotosCollectionView); 8 | #endregion 9 | 10 | #region Photo Detail Page 11 | public const string PhotoTitleLabel = nameof(PhotoTitleLabel); 12 | public const string PhotoImage = nameof(PhotoImage); 13 | #endregion 14 | 15 | #region Add Photo Page 16 | public const string AddPhotoPage_SaveButton = nameof(AddPhotoPage_SaveButton); 17 | public const string CancelButton = nameof(CancelButton); 18 | #endregion 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Mobile.Shared/Constants/BackendConstants.cs: -------------------------------------------------------------------------------- 1 | namespace AzureBlobStorageSampleApp.Mobile.Shared 2 | { 3 | public static class BackendConstants 4 | { 5 | public const string FunctionsAPIBaseUrl = "https://azureblobservicefunctions.azurewebsites.net/api"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Mobile.Shared/Constants/PageTitles.cs: -------------------------------------------------------------------------------- 1 | namespace AzureBlobStorageSampleApp.Mobile.Shared 2 | { 3 | public static class PageTitles 4 | { 5 | public const string PhotoListPage = "Photos"; 6 | public const string PhotoDetailsPage = "Photo"; 7 | public const string AddPhotoPage = "New Photo"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Shared/AzureBlobStorageSampleApp.Shared.projitems: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 5 | true 6 | {5C56AF55-408C-4FEE-8311-641762F75FE0} 7 | 8 | 9 | AzureBlobStorageSampleApp.Shared 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Shared/AzureBlobStorageSampleApp.Shared.shproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {5C56AF55-408C-4FEE-8311-641762F75FE0} 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Shared/Models/IBaseModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AzureBlobStorageSampleApp.Shared 4 | { 5 | public interface IBaseModel 6 | { 7 | string Id { get; } 8 | DateTimeOffset UpdatedAt { get; } 9 | DateTimeOffset CreatedAt { get; } 10 | bool IsDeleted { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.Shared/Models/PhotoModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | #if BACKEND 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | #elif MOBILE 6 | using SQLite; 7 | #endif 8 | 9 | namespace AzureBlobStorageSampleApp.Shared 10 | { 11 | #if BACKEND 12 | [Table("PhotoModels")] 13 | #endif 14 | public record PhotoModel : IBaseModel 15 | { 16 | public PhotoModel() => Id = Guid.NewGuid().ToString(); 17 | 18 | #if MOBILE 19 | [PrimaryKey, Unique] 20 | #else 21 | [Key] 22 | #endif 23 | public string Id { get; init; } 24 | public DateTimeOffset CreatedAt { get; init; } 25 | public DateTimeOffset UpdatedAt { get; init; } 26 | public bool IsDeleted { get; init; } 27 | public string Url { get; init; } = string.Empty; 28 | public string Title { get; init; } = string.Empty; 29 | } 30 | } 31 | 32 | namespace System.Runtime.CompilerServices 33 | { 34 | [ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)] 35 | public record IsExternalInit; 36 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/AppInitializer.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.UITest; 2 | 3 | namespace AzureBlobStorageSampleApp.UITests 4 | { 5 | public static class AppInitializer 6 | { 7 | public static IApp StartApp(Platform platform) 8 | { 9 | switch(platform) 10 | { 11 | case Platform.Android: 12 | return ConfigureApp.Android.StartApp(); 13 | case Platform.iOS: 14 | return ConfigureApp.iOS.StartApp(); 15 | default: 16 | throw new System.NotSupportedException(); 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/AzureBlobStorageSampleApp.UITests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6} 7 | Library 8 | AzureBlobStorageSampleApp.UITests 9 | AzureBlobStorageSampleApp.UITests 10 | v4.8 11 | 12 | 13 | true 14 | full 15 | false 16 | bin\Debug 17 | DEBUG; 18 | prompt 19 | 4 20 | 21 | 22 | true 23 | bin\Release 24 | prompt 25 | 4 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/Pages/AddPhotosPage.cs: -------------------------------------------------------------------------------- 1 | using AzureBlobStorageSampleApp.Mobile.Shared; 2 | 3 | using Xamarin.UITest; 4 | 5 | using Query = System.Func; 6 | 7 | namespace AzureBlobStorageSampleApp.UITests 8 | { 9 | public class AddPhotosPage : BasePage 10 | { 11 | readonly Query _saveButton, _cancelButton; 12 | 13 | public AddPhotosPage(IApp app) : base(app, PageTitles.AddPhotoPage) 14 | { 15 | _saveButton = x => x.Marked(AutomationIdConstants.AddPhotoPage_SaveButton); 16 | _cancelButton = x => x.Marked(AutomationIdConstants.CancelButton); 17 | } 18 | 19 | public void TapSaveButton() 20 | { 21 | App.Tap(_saveButton); 22 | App.Screenshot("Save Button Tapped"); 23 | } 24 | 25 | public void TapCancelButton() 26 | { 27 | App.Tap(_cancelButton); 28 | App.Screenshot("Cancel Button Tapped"); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/Pages/BasePage.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.UITest; 2 | 3 | namespace AzureBlobStorageSampleApp.UITests 4 | { 5 | public abstract class BasePage 6 | { 7 | protected BasePage(IApp app, string pageTitle) => (App, Title) = (app, pageTitle); 8 | 9 | public string Title { get; } 10 | protected IApp App { get; } 11 | 12 | public virtual void WaitForPageToLoad() => App.WaitForElement(Title); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/Pages/PhotoDetailPage.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | using Xamarin.UITest; 4 | 5 | using AzureBlobStorageSampleApp.Mobile.Shared; 6 | 7 | using Query = System.Func; 8 | 9 | namespace AzureBlobStorageSampleApp.UITests 10 | { 11 | public class PhotoDetailPage : BasePage 12 | { 13 | readonly Query _photoTitleLabel, _photoImage; 14 | 15 | public PhotoDetailPage(IApp app) : base(app, PageTitles.PhotoDetailsPage) 16 | { 17 | _photoTitleLabel = x => x.Marked(AutomationIdConstants.PhotoTitleLabel); 18 | _photoImage = x => x.Marked(AutomationIdConstants.PhotoImage); 19 | } 20 | 21 | public string PhotoTitle => App.Query(_photoTitleLabel).First().Text; 22 | 23 | public void WaitForImageToAppear() 24 | { 25 | App.WaitForElement(_photoImage); 26 | App.Screenshot("Image Appeared"); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/Pages/PhotoListPage.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.UITest; 2 | 3 | using AzureBlobStorageSampleApp.Mobile.Shared; 4 | 5 | using Query = System.Func; 6 | 7 | namespace AzureBlobStorageSampleApp.UITests 8 | { 9 | public class PhotoListPage : BasePage 10 | { 11 | readonly Query _photoListView, _addPhotoButton; 12 | 13 | public PhotoListPage(IApp app) : base(app, PageTitles.PhotoListPage) 14 | { 15 | _photoListView = x => x.Marked(AutomationIdConstants.PhotosCollectionView); 16 | _addPhotoButton = x => x.Marked(AutomationIdConstants.AddPhotoButton); 17 | } 18 | 19 | public void TapAddPhotoButton() 20 | { 21 | App.Tap(_addPhotoButton); 22 | App.Screenshot("Add Photo Button Tapped"); 23 | } 24 | 25 | public void SelectPhoto(string photoTitle) 26 | { 27 | App.ScrollDown(photoTitle); 28 | App.Tap(photoTitle); 29 | App.Screenshot($"Tapped {photoTitle} From List"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/Tests/BaseTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | using Xamarin.UITest; 5 | 6 | namespace AzureBlobStorageSampleApp.UITests 7 | { 8 | [TestFixture(Platform.iOS)] 9 | [TestFixture(Platform.Android)] 10 | public abstract class BaseTest 11 | { 12 | AddPhotosPage? _addPhotosPage; 13 | PhotoDetailPage? _photoDetailPage; 14 | PhotoListPage? _photoListPage; 15 | IApp? _app; 16 | 17 | protected BaseTest(Platform platform) => Platform = platform; 18 | 19 | protected Platform Platform { get; } 20 | 21 | protected AddPhotosPage AddPhotosPage => _addPhotosPage ?? throw new NullReferenceException(); 22 | protected PhotoDetailPage PhotoDetailPage => _photoDetailPage ?? throw new NullReferenceException(); 23 | protected PhotoListPage PhotoListPage => _photoListPage ?? throw new NullReferenceException(); 24 | protected IApp App => _app ?? throw new NullReferenceException(); 25 | 26 | [SetUp] 27 | protected virtual void BeforeEachTest() 28 | { 29 | _app = AppInitializer.StartApp(Platform); 30 | 31 | _addPhotosPage = new AddPhotosPage(App); 32 | _photoDetailPage = new PhotoDetailPage(App); 33 | _photoListPage = new PhotoListPage(App); 34 | 35 | App.Screenshot("App Launched"); 36 | } 37 | 38 | [TearDown] 39 | protected virtual void AfterEachTest() 40 | { 41 | 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/Tests/Tests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | 3 | using Xamarin.UITest; 4 | 5 | namespace AzureBlobStorageSampleApp.UITests 6 | { 7 | public class Tests : BaseTest 8 | { 9 | public Tests(Platform platform) : base(platform) 10 | { 11 | } 12 | 13 | [Test] 14 | public void AppLaunches() 15 | { 16 | 17 | } 18 | 19 | [TestCase("Punday")] 20 | [TestCase("Dog Toy")] 21 | [TestCase("Brandon")] 22 | public void VerifyPhoto(string photoTitle) 23 | { 24 | //Arrange 25 | 26 | //Act 27 | PhotoListPage.SelectPhoto(photoTitle); 28 | 29 | PhotoDetailPage.WaitForPageToLoad(); 30 | PhotoDetailPage.WaitForImageToAppear(); 31 | 32 | //Assert 33 | Assert.AreEqual(photoTitle, PhotoDetailPage.PhotoTitle); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.UITests/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/AppDelegate.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | using Foundation; 3 | 4 | namespace AzureBlobStorageSampleApp.iOS 5 | { 6 | [Register(nameof(AppDelegate))] 7 | public class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate 8 | { 9 | public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions) 10 | { 11 | global::Xamarin.Forms.Forms.Init(); 12 | FFImageLoading.Forms.Platform.CachedImageRenderer.Init(); 13 | FFImageLoading.Forms.Platform.CachedImageRenderer.InitImageSourceHandler(); 14 | 15 | #if DEBUG 16 | Xamarin.Calabash.Start(); 17 | #endif 18 | LoadApplication(new App()); 19 | 20 | return base.FinishedLaunching(uiApplication, launchOptions); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "idiom": "iphone", 5 | "size": "29x29", 6 | "scale": "1x" 7 | }, 8 | { 9 | "idiom": "iphone", 10 | "size": "29x29", 11 | "scale": "2x" 12 | }, 13 | { 14 | "idiom": "iphone", 15 | "size": "29x29", 16 | "scale": "3x" 17 | }, 18 | { 19 | "idiom": "iphone", 20 | "size": "40x40", 21 | "scale": "2x" 22 | }, 23 | { 24 | "idiom": "iphone", 25 | "size": "40x40", 26 | "scale": "3x" 27 | }, 28 | { 29 | "idiom": "iphone", 30 | "size": "57x57", 31 | "scale": "1x" 32 | }, 33 | { 34 | "idiom": "iphone", 35 | "size": "57x57", 36 | "scale": "2x" 37 | }, 38 | { 39 | "idiom": "iphone", 40 | "size": "60x60", 41 | "scale": "2x" 42 | }, 43 | { 44 | "idiom": "iphone", 45 | "size": "60x60", 46 | "scale": "3x" 47 | }, 48 | { 49 | "idiom": "ipad", 50 | "size": "29x29", 51 | "scale": "1x" 52 | }, 53 | { 54 | "idiom": "ipad", 55 | "size": "29x29", 56 | "scale": "2x" 57 | }, 58 | { 59 | "idiom": "ipad", 60 | "size": "40x40", 61 | "scale": "1x" 62 | }, 63 | { 64 | "idiom": "ipad", 65 | "size": "40x40", 66 | "scale": "2x" 67 | }, 68 | { 69 | "idiom": "ipad", 70 | "size": "50x50", 71 | "scale": "1x" 72 | }, 73 | { 74 | "idiom": "ipad", 75 | "size": "50x50", 76 | "scale": "2x" 77 | }, 78 | { 79 | "idiom": "ipad", 80 | "size": "72x72", 81 | "scale": "1x" 82 | }, 83 | { 84 | "idiom": "ipad", 85 | "size": "72x72", 86 | "scale": "2x" 87 | }, 88 | { 89 | "idiom": "ipad", 90 | "size": "76x76", 91 | "scale": "1x" 92 | }, 93 | { 94 | "idiom": "ipad", 95 | "size": "76x76", 96 | "scale": "2x" 97 | }, 98 | { 99 | "size": "24x24", 100 | "idiom": "watch", 101 | "scale": "2x", 102 | "role": "notificationCenter", 103 | "subtype": "38mm" 104 | }, 105 | { 106 | "size": "27.5x27.5", 107 | "idiom": "watch", 108 | "scale": "2x", 109 | "role": "notificationCenter", 110 | "subtype": "42mm" 111 | }, 112 | { 113 | "size": "29x29", 114 | "idiom": "watch", 115 | "role": "companionSettings", 116 | "scale": "2x" 117 | }, 118 | { 119 | "size": "29x29", 120 | "idiom": "watch", 121 | "role": "companionSettings", 122 | "scale": "3x" 123 | }, 124 | { 125 | "size": "40x40", 126 | "idiom": "watch", 127 | "scale": "2x", 128 | "role": "appLauncher", 129 | "subtype": "38mm" 130 | }, 131 | { 132 | "size": "44x44", 133 | "idiom": "watch", 134 | "scale": "2x", 135 | "role": "longLook", 136 | "subtype": "42mm" 137 | }, 138 | { 139 | "size": "86x86", 140 | "idiom": "watch", 141 | "scale": "2x", 142 | "role": "quickLook", 143 | "subtype": "38mm" 144 | }, 145 | { 146 | "size": "98x98", 147 | "idiom": "watch", 148 | "scale": "2x", 149 | "role": "quickLook", 150 | "subtype": "42mm" 151 | } 152 | ], 153 | "info": { 154 | "version": 1, 155 | "author": "xcode" 156 | } 157 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/AzureBlobStorageSampleApp.iOS.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | iPhoneSimulator 6 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6} 7 | {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 8 | Exe 9 | AzureBlobStorageSampleApp.iOS 10 | AzureBlobStorageSampleApp.iOS 11 | Resources 12 | 13 | 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\iPhoneSimulator\Debug 20 | DEBUG;ENABLE_TEST_CLOUD; 21 | prompt 22 | 4 23 | iPhone Developer 24 | true 25 | true 26 | true 27 | true 28 | 55168 29 | None 30 | x86_64 31 | NSUrlSessionHandler 32 | x86 33 | 34 | 35 | pdbonly 36 | true 37 | bin\iPhone\Release 38 | prompt 39 | 4 40 | iPhone Developer 41 | true 42 | Entitlements.plist 43 | SdkOnly 44 | ARMv7, ARM64 45 | NSUrlSessionHandler 46 | x86 47 | 48 | 49 | pdbonly 50 | true 51 | bin\iPhoneSimulator\Release 52 | prompt 53 | 4 54 | iPhone Developer 55 | true 56 | None 57 | x86_64 58 | NSUrlSessionHandler 59 | x86 60 | 61 | 62 | true 63 | full 64 | false 65 | bin\iPhone\Debug 66 | DEBUG;ENABLE_TEST_CLOUD; 67 | prompt 68 | 4 69 | iPhone Developer 70 | true 71 | true 72 | true 73 | true 74 | true 75 | true 76 | Entitlements.plist 77 | 60244 78 | SdkOnly 79 | ARMv7, ARM64 80 | NSUrlSessionHandler 81 | x86 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | false 106 | 107 | 108 | false 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B} 130 | AzureBlobStorageSampleApp 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/Custom Renderers/EntryCustomRederer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq; 4 | using AzureBlobStorageSampleApp.iOS; 5 | using UIKit; 6 | using Xamarin.Essentials; 7 | using Xamarin.Forms; 8 | using Xamarin.Forms.Platform.iOS; 9 | 10 | [assembly: ExportRenderer(typeof(Entry), typeof(EntryCustomRederer))] 11 | namespace AzureBlobStorageSampleApp.iOS 12 | { 13 | public class EntryCustomRederer : EntryRenderer 14 | { 15 | protected override void OnElementChanged(ElementChangedEventArgs e) 16 | { 17 | base.OnElementChanged(e); 18 | 19 | if (e.OldElement != null && Control != null) 20 | Control.AllEditingEvents -= HandleAllEditingEvents; 21 | 22 | if (e.NewElement != null && Control != null) 23 | Control.AllEditingEvents += HandleAllEditingEvents; 24 | } 25 | 26 | protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e) 27 | { 28 | base.OnElementPropertyChanged(sender, e); 29 | 30 | if (Control != null) 31 | { 32 | Control.Layer.BorderColor = UIColor.LightGray.CGColor; 33 | Control.Layer.BorderWidth = 0.25f; 34 | Control.Layer.CornerRadius = 5; 35 | } 36 | } 37 | 38 | void HandleAllEditingEvents(object sender, EventArgs e) 39 | { 40 | if (AppInfo.RequestedTheme is AppTheme.Dark 41 | && Control.Subviews.OfType().FirstOrDefault() is UIButton clearButton 42 | && clearButton.CurrentImage.ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate) is UIImage clearButtonImage) 43 | { 44 | clearButton.SetImage(clearButtonImage, UIControlState.Normal); 45 | clearButton.TintColor = UIColor.DarkGray; 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/Entitlements.plist: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDisplayName 6 | AzureBlobStorageSampleApp 7 | CFBundleName 8 | AzureBlobStorageSampleApp 9 | CFBundleIdentifier 10 | com.minnick.AzureBlobStorageSampleApp 11 | CFBundleShortVersionString 12 | 1.0 13 | CFBundleVersion 14 | 1.0 15 | LSRequiresIPhoneOS 16 | 17 | MinimumOSVersion 18 | 13.0 19 | UIDeviceFamily 20 | 21 | 1 22 | 2 23 | 24 | UILaunchStoryboardName 25 | LaunchScreen 26 | UIRequiredDeviceCapabilities 27 | 28 | armv7 29 | 30 | UISupportedInterfaceOrientations 31 | 32 | UIInterfaceOrientationPortrait 33 | UIInterfaceOrientationLandscapeLeft 34 | UIInterfaceOrientationLandscapeRight 35 | 36 | UISupportedInterfaceOrientations~ipad 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationPortraitUpsideDown 40 | UIInterfaceOrientationLandscapeLeft 41 | UIInterfaceOrientationLandscapeRight 42 | 43 | XSAppIconAssets 44 | Assets.xcassets/AppIcon.appiconset 45 | NSCameraUsageDescription 46 | Use the camera to take a new photo 47 | UIViewControllerBasedStatusBarAppearance 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/Main.cs: -------------------------------------------------------------------------------- 1 | using UIKit; 2 | 3 | namespace AzureBlobStorageSampleApp.iOS 4 | { 5 | public class Application 6 | { 7 | static void Main(string[] args) => UIApplication.Main(args, null, nameof(AppDelegate)); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.iOS/app.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.1.31911.260 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureBlobStorageSampleApp.iOS", "AzureBlobStorageSampleApp.iOS\AzureBlobStorageSampleApp.iOS.csproj", "{23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Mobile", "Mobile", "{9AEDCF1A-B053-4A8E-8FCE-C507F9DEA75D}" 8 | EndProject 9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Backend", "Backend", "{44790D75-6C29-4F49-A383-2C8D30492FDD}" 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{00247D84-414E-4F13-AF0D-5B0E1ED69600}" 12 | EndProject 13 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "AzureBlobStorageSampleApp.Shared", "AzureBlobStorageSampleApp.Shared\AzureBlobStorageSampleApp.Shared.shproj", "{5C56AF55-408C-4FEE-8311-641762F75FE0}" 14 | EndProject 15 | Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "AzureBlobStorageSampleApp.Mobile.Shared", "AzureBlobStorageSampleApp.Mobile.Shared\AzureBlobStorageSampleApp.Mobile.Shared.shproj", "{FDA2EE68-4202-4C8A-BC75-6B41E95EA3CC}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureBlobStorageSampleApp.Functions", "AzureBlobStorageSampleApp.Functions\AzureBlobStorageSampleApp.Functions.csproj", "{9222A54C-E421-49B6-998B-678CC4E76203}" 18 | EndProject 19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureBlobStorageSampleApp.Android", "AzureBlobStorageSampleApp.Android\AzureBlobStorageSampleApp.Android.csproj", "{9AA352A4-133F-4D31-8517-91B11010DD0C}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureBlobStorageSampleApp", "AzureBlobStorageSampleApp\AzureBlobStorageSampleApp.csproj", "{389B6A9C-09CD-470A-B8DA-36FFAF39F52B}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AzureBlobStorageSampleApp.UITests", "AzureBlobStorageSampleApp.UITests\AzureBlobStorageSampleApp.UITests.csproj", "{FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}" 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6B91C6CE-E49F-4E26-9C29-1769AA48B401}" 26 | ProjectSection(SolutionItems) = preProject 27 | Directory.Build.props = Directory.Build.props 28 | EndProjectSection 29 | EndProject 30 | Global 31 | GlobalSection(SharedMSBuildProjectFiles) = preSolution 32 | AzureBlobStorageSampleApp.Mobile.Shared\AzureBlobStorageSampleApp.Mobile.Shared.projitems*{389b6a9c-09cd-470a-b8da-36ffaf39f52b}*SharedItemsImports = 5 33 | AzureBlobStorageSampleApp.Shared\AzureBlobStorageSampleApp.Shared.projitems*{389b6a9c-09cd-470a-b8da-36ffaf39f52b}*SharedItemsImports = 5 34 | AzureBlobStorageSampleApp.Shared\AzureBlobStorageSampleApp.Shared.projitems*{5c56af55-408c-4fee-8311-641762f75fe0}*SharedItemsImports = 13 35 | AzureBlobStorageSampleApp.Shared\AzureBlobStorageSampleApp.Shared.projitems*{9222a54c-e421-49b6-998b-678cc4e76203}*SharedItemsImports = 5 36 | AzureBlobStorageSampleApp.Mobile.Shared\AzureBlobStorageSampleApp.Mobile.Shared.projitems*{fd2382a9-2f14-4719-afe7-1a3efac905b6}*SharedItemsImports = 4 37 | AzureBlobStorageSampleApp.Mobile.Shared\AzureBlobStorageSampleApp.Mobile.Shared.projitems*{fda2ee68-4202-4c8a-bc75-6b41e95ea3cc}*SharedItemsImports = 13 38 | EndGlobalSection 39 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 40 | Debug|Any CPU = Debug|Any CPU 41 | Debug|iPhone = Debug|iPhone 42 | Debug|iPhoneSimulator = Debug|iPhoneSimulator 43 | Release|Any CPU = Release|Any CPU 44 | Release|iPhone = Release|iPhone 45 | Release|iPhoneSimulator = Release|iPhoneSimulator 46 | EndGlobalSection 47 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 48 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator 49 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator 50 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Debug|iPhone.ActiveCfg = Debug|iPhone 51 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Debug|iPhone.Build.0 = Debug|iPhone 52 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator 53 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator 54 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Release|Any CPU.ActiveCfg = Release|iPhone 55 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Release|Any CPU.Build.0 = Release|iPhone 56 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Release|iPhone.ActiveCfg = Release|iPhone 57 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Release|iPhone.Build.0 = Release|iPhone 58 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator 59 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator 60 | {9222A54C-E421-49B6-998B-678CC4E76203}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {9222A54C-E421-49B6-998B-678CC4E76203}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {9222A54C-E421-49B6-998B-678CC4E76203}.Debug|iPhone.ActiveCfg = Debug|Any CPU 63 | {9222A54C-E421-49B6-998B-678CC4E76203}.Debug|iPhone.Build.0 = Debug|Any CPU 64 | {9222A54C-E421-49B6-998B-678CC4E76203}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 65 | {9222A54C-E421-49B6-998B-678CC4E76203}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 66 | {9222A54C-E421-49B6-998B-678CC4E76203}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {9222A54C-E421-49B6-998B-678CC4E76203}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {9222A54C-E421-49B6-998B-678CC4E76203}.Release|iPhone.ActiveCfg = Release|Any CPU 69 | {9222A54C-E421-49B6-998B-678CC4E76203}.Release|iPhone.Build.0 = Release|Any CPU 70 | {9222A54C-E421-49B6-998B-678CC4E76203}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 71 | {9222A54C-E421-49B6-998B-678CC4E76203}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 72 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU 75 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Debug|iPhone.ActiveCfg = Debug|Any CPU 76 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Debug|iPhone.Build.0 = Debug|Any CPU 77 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 78 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 79 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Release|iPhone.ActiveCfg = Release|Any CPU 82 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Release|iPhone.Build.0 = Release|Any CPU 83 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 84 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 85 | {9AA352A4-133F-4D31-8517-91B11010DD0C}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU 86 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 87 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Debug|Any CPU.Build.0 = Debug|Any CPU 88 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Debug|iPhone.ActiveCfg = Debug|Any CPU 89 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Debug|iPhone.Build.0 = Debug|Any CPU 90 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 91 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 92 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Release|Any CPU.ActiveCfg = Release|Any CPU 93 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Release|Any CPU.Build.0 = Release|Any CPU 94 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Release|iPhone.ActiveCfg = Release|Any CPU 95 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Release|iPhone.Build.0 = Release|Any CPU 96 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 97 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 98 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 99 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Debug|Any CPU.Build.0 = Debug|Any CPU 100 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Debug|iPhone.ActiveCfg = Debug|Any CPU 101 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Debug|iPhone.Build.0 = Debug|Any CPU 102 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU 103 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU 104 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Release|Any CPU.ActiveCfg = Release|Any CPU 105 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Release|Any CPU.Build.0 = Release|Any CPU 106 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Release|iPhone.ActiveCfg = Release|Any CPU 107 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Release|iPhone.Build.0 = Release|Any CPU 108 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU 109 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6}.Release|iPhoneSimulator.Build.0 = Release|Any CPU 110 | EndGlobalSection 111 | GlobalSection(SolutionProperties) = preSolution 112 | HideSolutionNode = FALSE 113 | EndGlobalSection 114 | GlobalSection(NestedProjects) = preSolution 115 | {23DC86F6-BDBD-45F0-AFDA-1914DAB289F6} = {9AEDCF1A-B053-4A8E-8FCE-C507F9DEA75D} 116 | {5C56AF55-408C-4FEE-8311-641762F75FE0} = {00247D84-414E-4F13-AF0D-5B0E1ED69600} 117 | {FDA2EE68-4202-4C8A-BC75-6B41E95EA3CC} = {9AEDCF1A-B053-4A8E-8FCE-C507F9DEA75D} 118 | {9222A54C-E421-49B6-998B-678CC4E76203} = {44790D75-6C29-4F49-A383-2C8D30492FDD} 119 | {9AA352A4-133F-4D31-8517-91B11010DD0C} = {9AEDCF1A-B053-4A8E-8FCE-C507F9DEA75D} 120 | {389B6A9C-09CD-470A-B8DA-36FFAF39F52B} = {9AEDCF1A-B053-4A8E-8FCE-C507F9DEA75D} 121 | {FD2382A9-2F14-4719-AFE7-1A3EFAC905B6} = {9AEDCF1A-B053-4A8E-8FCE-C507F9DEA75D} 122 | EndGlobalSection 123 | GlobalSection(ExtensibilityGlobals) = postSolution 124 | SolutionGuid = {B507A9D9-2503-4C31-909E-C5C99623C4C6} 125 | EndGlobalSection 126 | EndGlobal 127 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/App.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms.PlatformConfiguration; 2 | using Xamarin.Forms.PlatformConfiguration.iOSSpecific; 3 | 4 | namespace AzureBlobStorageSampleApp 5 | { 6 | public class App : Xamarin.Forms.Application 7 | { 8 | public App() 9 | { 10 | var navigationPage = new BaseNavigationPage(new PhotoListPage()); 11 | navigationPage.On().SetPrefersLargeTitles(true); 12 | 13 | MainPage = navigationPage; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/AzureBlobStorageSampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | TRACE;DEBUG;NETSTANDARD1_6;MOBILE 8 | 9 | 10 | 11 | 12 | TRACE;RELEASE;NETSTANDARD1_6;MOBILE 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 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Constants/ColorConstants.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | 3 | namespace AzureBlobStorageSampleApp 4 | { 5 | public static class ColorConstants 6 | { 7 | public static Color NavigationBarTextColor => Color.White; 8 | public static Color NavigationBarBackgroundColor => Color.FromHex("E3553D"); 9 | public static Color PageBackgroundColor => Color.FromHex("F8C6BB"); 10 | public static Color TextColor => Color.FromHex("1B2A38"); 11 | public static Color DetailColor => Color.FromHex("2B3E50"); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Database/BaseDatabase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Polly; 6 | using SQLite; 7 | using Xamarin.Essentials; 8 | 9 | namespace AzureBlobStorageSampleApp 10 | { 11 | public abstract class BaseDatabase 12 | { 13 | static readonly string _databasePath = Path.Combine(FileSystem.AppDataDirectory, $"{nameof(AzureBlobStorageSampleApp)}.db3"); 14 | 15 | static readonly Lazy _databaseConnectionHolder = 16 | new Lazy(() => new SQLiteAsyncConnection(_databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.SharedCache)); 17 | 18 | static SQLiteAsyncConnection DatabaseConnection => _databaseConnectionHolder.Value; 19 | 20 | protected static async Task ExecuteDatabaseFunction(Func> action, int numRetries = 12) 21 | { 22 | if (!DatabaseConnection.TableMappings.Any(x => x.MappedType == typeof(TDataType))) 23 | { 24 | await DatabaseConnection.EnableWriteAheadLoggingAsync().ConfigureAwait(false); 25 | await DatabaseConnection.CreateTablesAsync(CreateFlags.None, typeof(TDataType)).ConfigureAwait(false); 26 | } 27 | 28 | return await Policy.Handle().WaitAndRetryAsync(numRetries, pollyRetryAttempt).ExecuteAsync(() => action(DatabaseConnection)).ConfigureAwait(false); 29 | 30 | static TimeSpan pollyRetryAttempt(int attemptNumber) => TimeSpan.FromMilliseconds(Math.Pow(2, attemptNumber)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Database/PhotoDatabase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using System.Collections.Generic; 3 | 4 | using AzureBlobStorageSampleApp.Shared; 5 | 6 | namespace AzureBlobStorageSampleApp 7 | { 8 | public abstract class PhotoDatabase : BaseDatabase 9 | { 10 | public static Task SavePhoto(PhotoModel photo) => ExecuteDatabaseFunction(databaseConnection => databaseConnection.InsertOrReplaceAsync(photo)); 11 | 12 | public static Task GetPhotoCount() => ExecuteDatabaseFunction(databaseConnection => databaseConnection.Table().CountAsync()); 13 | 14 | public static Task> GetAllPhotos() => ExecuteDatabaseFunction>(async databaseConnection => await databaseConnection.Table().ToListAsync().ConfigureAwait(false)); 15 | 16 | public static Task GetPhoto(string id) => ExecuteDatabaseFunction(databaseConnection => databaseConnection.Table().Where(x => x.Id.Equals(id)).FirstOrDefaultAsync()); 17 | 18 | public static Task DeletePhoto(PhotoModel photo) 19 | { 20 | photo = photo with { IsDeleted = true }; 21 | 22 | return ExecuteDatabaseFunction(databaseConnection => databaseConnection.UpdateAsync(photo)); 23 | } 24 | 25 | #if DEBUG 26 | public static Task RemovePhoto(PhotoModel photo) => ExecuteDatabaseFunction(databaseConnection => databaseConnection.DeleteAsync(photo)); 27 | #endif 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Pages/AddPhotoPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using AsyncAwaitBestPractices.MVVM; 4 | using AzureBlobStorageSampleApp.Mobile.Shared; 5 | using FFImageLoading.Forms; 6 | using Xamarin.Essentials; 7 | using Xamarin.Forms; 8 | using Xamarin.CommunityToolkit.Markup; 9 | 10 | namespace AzureBlobStorageSampleApp 11 | { 12 | public class AddPhotoPage : BaseContentPage 13 | { 14 | public AddPhotoPage() 15 | { 16 | ViewModel.NoCameraFound += HandleNoCameraFound; 17 | ViewModel.SavePhotoFailed += HandleSavePhotoFailed; 18 | ViewModel.SavePhotoCompleted += HandleSavePhotoCompleted; 19 | 20 | this.Bind(TitleProperty, nameof(AddPhotoViewModel.PhotoTitle)); 21 | 22 | Title = PageTitles.AddPhotoPage; 23 | Padding = new Thickness(20); 24 | 25 | Content = new ScrollView 26 | { 27 | Content = new StackLayout 28 | { 29 | Spacing = 20, 30 | 31 | Children = 32 | { 33 | new CachedImage() 34 | .Bind(CachedImage.SourceProperty, nameof(AddPhotoViewModel.PhotoImageSource)), 35 | 36 | new Entry { ClearButtonVisibility = ClearButtonVisibility.WhileEditing, Placeholder = "Title", BackgroundColor = Color.White, TextColor = ColorConstants.TextColor }.FillExpand() 37 | .Bind(Entry.TextProperty, nameof(AddPhotoViewModel.PhotoTitle)), 38 | 39 | new AddPhotoPageButton("Take Photo") 40 | .Bind(Button.CommandProperty,nameof(AddPhotoViewModel.TakePhotoCommand)) 41 | .Bind(IsEnabledProperty,nameof(AddPhotoViewModel.IsPhotoSaving), convert: isPhotoSaving => !isPhotoSaving), 42 | 43 | new ActivityIndicator() 44 | .Bind(IsVisibleProperty, nameof(AddPhotoViewModel.IsPhotoSaving)) 45 | .Bind(ActivityIndicator.IsRunningProperty, nameof(AddPhotoViewModel.IsPhotoSaving)) 46 | } 47 | }.Top().FillExpandHorizontal().Assign(out StackLayout stackLayout) 48 | }; 49 | 50 | if (Device.RuntimePlatform is Device.iOS) 51 | { 52 | //Add title to UIModalPresentationStyle.FormSheet on iOS 53 | stackLayout.Children.Insert(0, new Label { Text = PageTitles.AddPhotoPage }.Font(size: 24, bold: true).Margins(0, 24, 5, 0)); 54 | 55 | stackLayout.Children.Add(new AddPhotoPageButton("Save") 56 | .Bind(IsVisibleProperty, nameof(AddPhotoViewModel.IsPhotoSaving), convert: isSaving => !isSaving) 57 | .Bind(Button.CommandProperty, nameof(AddPhotoViewModel.SavePhotoCommand))); 58 | } 59 | else 60 | { 61 | //Save Button can be added to the Navigation Bar 62 | ToolbarItems.Add(new ToolbarItem 63 | { 64 | Text = "Save", 65 | Priority = 0, 66 | AutomationId = AutomationIdConstants.AddPhotoPage_SaveButton, 67 | }.Bind(MenuItem.CommandProperty, nameof(AddPhotoViewModel.SavePhotoCommand))); 68 | 69 | //Cancel Button only needed for Android becuase iOS can swipe down to return to previous page 70 | ToolbarItems.Add(new ToolbarItem 71 | { 72 | Text = "Cancel", 73 | Priority = 1, 74 | AutomationId = AutomationIdConstants.CancelButton, 75 | Command = new AsyncCommand(ClosePage, _ => !ViewModel.IsPhotoSaving) 76 | }); 77 | } 78 | } 79 | 80 | void HandleSavePhotoCompleted(object sender, EventArgs e) 81 | { 82 | MainThread.BeginInvokeOnMainThread(async () => 83 | { 84 | await DisplayAlert("Photo Saved", string.Empty, "OK"); 85 | await ClosePage(); 86 | }); 87 | } 88 | 89 | async void HandleSavePhotoFailed(object sender, string errorMessage) => await DisplayErrorMessage(errorMessage); 90 | 91 | async void HandleNoCameraFound(object sender, EventArgs e) => await DisplayErrorMessage("No Camera Found"); 92 | 93 | Task ClosePage() => MainThread.InvokeOnMainThreadAsync(Navigation.PopModalAsync); 94 | 95 | Task DisplayErrorMessage(string message) => 96 | MainThread.InvokeOnMainThreadAsync(() => DisplayAlert("Error", message, "Ok")); 97 | 98 | class AddPhotoPageButton : Button 99 | { 100 | public AddPhotoPageButton(string text) 101 | { 102 | Text = text; 103 | BackgroundColor = ColorConstants.NavigationBarBackgroundColor; 104 | TextColor = Color.White; 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Pages/Base/BaseContentPage.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | using Xamarin.Forms.PlatformConfiguration; 3 | using Xamarin.Forms.PlatformConfiguration.iOSSpecific; 4 | 5 | namespace AzureBlobStorageSampleApp 6 | { 7 | public abstract class BaseContentPage : ContentPage where T : BaseViewModel, new() 8 | { 9 | protected BaseContentPage() 10 | { 11 | On().SetModalPresentationStyle(UIModalPresentationStyle.FormSheet); 12 | BindingContext = ViewModel; 13 | BackgroundColor = ColorConstants.PageBackgroundColor; 14 | } 15 | 16 | protected T ViewModel { get; } = new T(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Pages/Base/BaseNavigationPage.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | 3 | namespace AzureBlobStorageSampleApp 4 | { 5 | public class BaseNavigationPage : NavigationPage 6 | { 7 | public BaseNavigationPage(Page root) : base(root) 8 | { 9 | BarBackgroundColor = ColorConstants.NavigationBarBackgroundColor; 10 | BarTextColor = ColorConstants.NavigationBarTextColor; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Pages/PhotoDetailsPage.cs: -------------------------------------------------------------------------------- 1 | using AzureBlobStorageSampleApp.Mobile.Shared; 2 | using AzureBlobStorageSampleApp.Shared; 3 | using FFImageLoading.Forms; 4 | using Xamarin.Forms; 5 | using Xamarin.CommunityToolkit.Markup; 6 | 7 | namespace AzureBlobStorageSampleApp 8 | { 9 | public class PhotoDetailsPage : BaseContentPage 10 | { 11 | public PhotoDetailsPage(PhotoModel selectedPhoto) 12 | { 13 | ViewModel.SetPhotoCommand?.Execute(selectedPhoto); 14 | 15 | Title = PageTitles.PhotoDetailsPage; 16 | 17 | Padding = new Thickness(20); 18 | 19 | Content = new StackLayout 20 | { 21 | Spacing = 20, 22 | 23 | Children = 24 | { 25 | new CachedImage { AutomationId = AutomationIdConstants.PhotoImage } 26 | .Bind(CachedImage.SourceProperty, nameof(PhotoDetailsViewModel.PhotoImageSource)), 27 | new PhotoDetailLabel(AutomationIdConstants.PhotoTitleLabel) 28 | .Bind(Label.TextProperty, nameof(PhotoDetailsViewModel.PhotoTitle)) 29 | } 30 | }.Center(); 31 | } 32 | 33 | class PhotoDetailLabel : Label 34 | { 35 | public PhotoDetailLabel(in string automationId) 36 | { 37 | AutomationId = automationId; 38 | TextColor = Color.FromHex("1B2A38"); 39 | HorizontalTextAlignment = TextAlignment.Center; 40 | FontAttributes = FontAttributes.Bold; 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Pages/PhotoListPage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using AzureBlobStorageSampleApp.Mobile.Shared; 4 | using AzureBlobStorageSampleApp.Shared; 5 | using Xamarin.Forms; 6 | using Xamarin.CommunityToolkit.Markup; 7 | 8 | namespace AzureBlobStorageSampleApp 9 | { 10 | public class PhotoListPage : BaseContentPage 11 | { 12 | public PhotoListPage() 13 | { 14 | ViewModel.RefreshFailed += HandleRefreshFailed; 15 | Title = PageTitles.PhotoListPage; 16 | 17 | ToolbarItems.Add(new ToolbarItem 18 | { 19 | Text = "+", 20 | AutomationId = AutomationIdConstants.AddPhotoButton 21 | }.Invoke(addPhotosButton => addPhotosButton.Clicked += HandleAddContactButtonClicked)); 22 | 23 | Content = new RefreshView 24 | { 25 | Content = new CollectionView 26 | { 27 | ItemTemplate = new PhotoDataTemplate(), 28 | SelectionMode = SelectionMode.Single, 29 | AutomationId = AutomationIdConstants.PhotosCollectionView, 30 | }.Bind(CollectionView.ItemsSourceProperty, nameof(PhotoListViewModel.AllPhotosList)) 31 | .Invoke(collectionView => collectionView.SelectionChanged += HandlePhotoCollectionSelectionChanged) 32 | 33 | }.Bind(RefreshView.IsRefreshingProperty, nameof(PhotoListViewModel.IsRefreshing)) 34 | .Bind(RefreshView.CommandProperty, nameof(PhotoListViewModel.RefreshCommand)); 35 | } 36 | 37 | protected override void OnAppearing() 38 | { 39 | base.OnAppearing(); 40 | 41 | var refreshView = (RefreshView)Content; 42 | refreshView.IsRefreshing = true; 43 | } 44 | 45 | async void HandlePhotoCollectionSelectionChanged(object sender, SelectionChangedEventArgs e) 46 | { 47 | var collectionView = (CollectionView)sender; 48 | collectionView.SelectedItem = null; 49 | 50 | if (e.CurrentSelection.FirstOrDefault() is PhotoModel selectedPhoto) 51 | { 52 | await Navigation.PushAsync(new PhotoDetailsPage(selectedPhoto)); 53 | } 54 | } 55 | 56 | async void HandleAddContactButtonClicked(object sender, EventArgs e) 57 | { 58 | //iOS uses UIModalPresentationStyle.FormSheet 59 | if (Device.RuntimePlatform is Device.iOS) 60 | await Navigation.PushModalAsync(new AddPhotoPage()); 61 | else 62 | await Navigation.PushModalAsync(new BaseNavigationPage(new AddPhotoPage())); 63 | } 64 | 65 | async void HandleRefreshFailed(object sender, Exception exception) => 66 | await DisplayAlert("Get Photos Failed", exception.ToString(), "OK"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Services/APIService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using AzureBlobStorageSampleApp.Mobile.Shared; 6 | using AzureBlobStorageSampleApp.Shared; 7 | using Polly; 8 | using Refit; 9 | using Xamarin.Essentials; 10 | 11 | namespace AzureBlobStorageSampleApp 12 | { 13 | static class APIService 14 | { 15 | readonly static Lazy _photosApiClientHolder = new Lazy(() => RefitExtensions.For(new HttpClient { BaseAddress = new Uri(BackendConstants.FunctionsAPIBaseUrl) })); 16 | 17 | static IPhotosAPI PhotosApiClient => _photosApiClientHolder.Value; 18 | 19 | public static Task> GetAllPhotoModels() => ExecutePollyFunction(PhotosApiClient.GetAllPhotoModels); 20 | 21 | public static async Task PostPhotoBlob(string photoTitle, FileResult photoMediaFile) 22 | { 23 | var fileStream = await photoMediaFile.OpenReadAsync().ConfigureAwait(false); 24 | return await ExecutePollyFunction(() => PhotosApiClient.PostPhotoBlob(photoTitle, new StreamPart(fileStream, $"{photoTitle}"))).ConfigureAwait(false); 25 | } 26 | 27 | static Task ExecutePollyFunction(Func> action, int numRetries = 3) 28 | { 29 | return Policy.Handle().WaitAndRetryAsync(numRetries, pollyRetryAttempt).ExecuteAsync(action); 30 | 31 | static TimeSpan pollyRetryAttempt(int attemptNumber) => TimeSpan.FromSeconds(Math.Pow(2, attemptNumber)); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Services/DatabaseSyncService.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using System.Collections.Generic; 4 | 5 | using AzureBlobStorageSampleApp.Shared; 6 | 7 | namespace AzureBlobStorageSampleApp 8 | { 9 | public static class DatabaseSyncService 10 | { 11 | public static async Task SyncRemoteAndLocalDatabases() 12 | { 13 | var (photoListFromLocalDatabase, photoListFromRemoteDatabase) = await GetAllSavedPhotos().ConfigureAwait(false); 14 | 15 | var (photosInLocalDatabaseButNotStoredRemotely, photosInRemoteDatabaseButNotStoredLocally, photosInBothDatabases) = GetMatchingModels(photoListFromLocalDatabase, photoListFromRemoteDatabase); 16 | 17 | var (photosToPatchToLocalDatabase, photosToPatchToRemoteDatabase) = GetModelsThatNeedUpdating(photoListFromLocalDatabase, photoListFromRemoteDatabase, photosInBothDatabases); 18 | 19 | await SavePhotos(photosInRemoteDatabaseButNotStoredLocally.Concat(photosToPatchToLocalDatabase).ToList(), photosInLocalDatabaseButNotStoredRemotely.Concat(photosToPatchToRemoteDatabase).ToList()).ConfigureAwait(false); 20 | } 21 | 22 | static async Task<(IReadOnlyList photoListFromLocalDatabase, 23 | IReadOnlyList photoListFromRemoteDatabase)> GetAllSavedPhotos() 24 | { 25 | var photoListFromLocalDatabaseTask = PhotoDatabase.GetAllPhotos(); 26 | var photoListFromRemoteDatabaseTask = APIService.GetAllPhotoModels(); 27 | 28 | await Task.WhenAll(photoListFromLocalDatabaseTask, photoListFromRemoteDatabaseTask).ConfigureAwait(false); 29 | 30 | return (await photoListFromLocalDatabaseTask.ConfigureAwait(false), 31 | await photoListFromRemoteDatabaseTask.ConfigureAwait(false)); 32 | } 33 | 34 | static (IReadOnlyList contactsInLocalDatabaseButNotStoredRemotely, 35 | IReadOnlyList contactsInRemoteDatabaseButNotStoredLocally, 36 | IReadOnlyList contactsInBothDatabases) GetMatchingModels(IEnumerable modelListFromLocalDatabase, 37 | IEnumerable modelListFromRemoteDatabase) where T : IBaseModel 38 | { 39 | var modelIdFromRemoteDatabaseList = modelListFromRemoteDatabase?.Select(x => x.Id).ToList() ?? Enumerable.Empty(); 40 | var modelIdFromLocalDatabaseList = modelListFromLocalDatabase?.Select(x => x.Id).ToList() ?? Enumerable.Empty(); 41 | 42 | var modelIdsInRemoteDatabaseButNotStoredLocally = modelIdFromRemoteDatabaseList?.Except(modelIdFromLocalDatabaseList)?.ToList() ?? Enumerable.Empty(); 43 | var modelIdsInLocalDatabaseButNotStoredRemotely = modelIdFromLocalDatabaseList?.Except(modelIdFromRemoteDatabaseList)?.ToList() ?? Enumerable.Empty(); 44 | var modelIdsInBothDatabases = modelIdFromRemoteDatabaseList?.Where(x => modelIdFromLocalDatabaseList?.Contains(x) ?? false).ToList() ?? Enumerable.Empty(); 45 | 46 | var modelsInRemoteDatabaseButNotStoredLocally = modelListFromRemoteDatabase?.Where(x => modelIdsInRemoteDatabaseButNotStoredLocally?.Contains(x?.Id) ?? false).ToList() ?? Enumerable.Empty(); 47 | var modelsInLocalDatabaseButNotStoredRemotely = modelListFromLocalDatabase?.Where(x => modelIdsInLocalDatabaseButNotStoredRemotely?.Contains(x?.Id) ?? false).ToList() ?? Enumerable.Empty(); 48 | 49 | var modelsInBothDatabases = modelListFromLocalDatabase?.Where(x => modelIdsInBothDatabases?.Contains(x?.Id) ?? false) 50 | .ToList() ?? Enumerable.Empty(); 51 | 52 | return ((modelsInLocalDatabaseButNotStoredRemotely ?? Enumerable.Empty()).ToList(), 53 | (modelsInRemoteDatabaseButNotStoredLocally ?? Enumerable.Empty()).ToList(), 54 | (modelsInBothDatabases ?? Enumerable.Empty()).ToList()); 55 | 56 | } 57 | 58 | static (IReadOnlyList contactsToPatchToLocalDatabase, 59 | IReadOnlyList contactsToPatchToRemoteDatabase) GetModelsThatNeedUpdating(IEnumerable modelListFromLocalDatabase, 60 | IEnumerable modelListFromRemoteDatabase, 61 | IEnumerable modelsFoundInBothDatabases) where T : IBaseModel 62 | { 63 | var modelsToPatchToRemoteDatabase = new List(); 64 | var modelsToPatchToLocalDatabase = new List(); 65 | foreach (var contact in modelsFoundInBothDatabases) 66 | { 67 | var modelFromLocalDatabase = modelListFromLocalDatabase.Where(x => x.Id.Equals(contact.Id)).FirstOrDefault(); 68 | var modelFromRemoteDatabase = modelListFromRemoteDatabase.Where(x => x.Id.Equals(contact.Id)).FirstOrDefault(); 69 | 70 | if (modelFromLocalDatabase?.UpdatedAt.CompareTo(modelFromRemoteDatabase?.UpdatedAt ?? default) > 0) 71 | modelsToPatchToRemoteDatabase.Add(modelFromLocalDatabase); 72 | else if (modelFromRemoteDatabase is not null && modelFromLocalDatabase?.UpdatedAt.CompareTo(modelFromRemoteDatabase.UpdatedAt) < 0) 73 | modelsToPatchToLocalDatabase.Add(modelFromRemoteDatabase); 74 | } 75 | 76 | return (modelsToPatchToLocalDatabase ?? Enumerable.Empty().ToList(), 77 | modelsToPatchToRemoteDatabase ?? Enumerable.Empty().ToList()); 78 | } 79 | 80 | static Task SavePhotos(IEnumerable photosToSaveToLocalDatabase, 81 | IEnumerable photosToSaveToRemoteDatabase) 82 | { 83 | var savephotoTaskList = new List(); 84 | 85 | foreach (var photo in photosToSaveToLocalDatabase) 86 | savephotoTaskList.Add(PhotoDatabase.SavePhoto(photo)); 87 | 88 | //ToDo Add Patch API 89 | 90 | return Task.WhenAll(savephotoTaskList); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Services/DebugServices.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace AzureBlobStorageSampleApp 7 | { 8 | public static class DebugServices 9 | { 10 | [Conditional("DEBUG")] 11 | public static void Log(Exception exception, 12 | IDictionary? properties = null, 13 | [CallerMemberName] string callerMemberName = "", 14 | [CallerLineNumber] int lineNumber = 0, 15 | [CallerFilePath] string filePath = "") 16 | { 17 | var fileName = System.IO.Path.GetFileName(filePath); 18 | 19 | Debug.WriteLine(exception.GetType()); 20 | Debug.WriteLine($"Error: {exception.Message}"); 21 | Debug.WriteLine($"Line Number: {lineNumber}"); 22 | Debug.WriteLine($"Caller Name: {callerMemberName}"); 23 | Debug.WriteLine($"File Name: {fileName}"); 24 | 25 | if (properties != null) 26 | { 27 | foreach (var property in properties) 28 | Debug.WriteLine($"{property.Key}: {property.Value}"); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Services/IPhotosAPI.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | 4 | using AzureBlobStorageSampleApp.Shared; 5 | 6 | using Refit; 7 | 8 | namespace AzureBlobStorageSampleApp 9 | { 10 | [Headers("Accept-Encoding: gzip", "Accept: application/json")] 11 | public interface IPhotosAPI 12 | { 13 | [Get("/GetPhotos")] 14 | Task> GetAllPhotoModels(); 15 | 16 | [Post("/PostBlob/{photoTitle}"), Multipart] 17 | Task PostPhotoBlob(string photoTitle, [AliasAs("photo")] StreamPart photoStream); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Services/RefitExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Serialization; 4 | using Refit; 5 | 6 | namespace AzureBlobStorageSampleApp 7 | { 8 | public static class RefitExtensions 9 | { 10 | public static T For(string hostUrl) => RestService.For(hostUrl, GetNewtonsoftJsonRefitSettings()); 11 | public static T For(HttpClient client) => RestService.For(client, GetNewtonsoftJsonRefitSettings()); 12 | 13 | public static RefitSettings GetNewtonsoftJsonRefitSettings() => new RefitSettings(new NewtonsoftJsonContentSerializer(new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() })); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/ViewModels/AddPhotoViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using AsyncAwaitBestPractices; 5 | using AsyncAwaitBestPractices.MVVM; 6 | using Xamarin.Essentials; 7 | 8 | namespace AzureBlobStorageSampleApp 9 | { 10 | public class AddPhotoViewModel : BaseViewModel 11 | { 12 | readonly WeakEventManager _noCameraFoundEventManager = new WeakEventManager(); 13 | readonly WeakEventManager _savePhotoCompletedEventManager = new WeakEventManager(); 14 | readonly WeakEventManager _savePhotoFailedEventManager = new WeakEventManager(); 15 | 16 | FileResult? _photoMediaFile; 17 | Xamarin.Forms.ImageSource? _photoImageSource; 18 | AsyncCommand? _savePhotoCommand, _takePhotoCommand; 19 | 20 | string _photoTitle = string.Empty; 21 | 22 | bool _isPhotoSaving; 23 | 24 | public event EventHandler NoCameraFound 25 | { 26 | add => _noCameraFoundEventManager.AddEventHandler(value); 27 | remove => _noCameraFoundEventManager.RemoveEventHandler(value); 28 | } 29 | 30 | public event EventHandler SavePhotoCompleted 31 | { 32 | add => _savePhotoCompletedEventManager.AddEventHandler(value); 33 | remove => _savePhotoCompletedEventManager.RemoveEventHandler(value); 34 | } 35 | 36 | public event EventHandler SavePhotoFailed 37 | { 38 | add => _savePhotoFailedEventManager.AddEventHandler(value); 39 | remove => _savePhotoFailedEventManager.RemoveEventHandler(value); 40 | } 41 | 42 | public AsyncCommand TakePhotoCommand => _takePhotoCommand ??= new AsyncCommand(ExecuteTakePhotoCommand, _ => !IsPhotoSaving); 43 | public AsyncCommand SavePhotoCommand => _savePhotoCommand ??= new AsyncCommand(() => ExecuteSavePhotoCommand(_photoMediaFile, PhotoTitle), 44 | _ => !IsPhotoSaving && !string.IsNullOrWhiteSpace(PhotoTitle) && PhotoImageSource != null); 45 | 46 | public bool IsPhotoSaving 47 | { 48 | get => _isPhotoSaving; 49 | set => SetProperty(ref _isPhotoSaving, value, async () => await UpdateCanExecute().ConfigureAwait(false)); 50 | } 51 | 52 | public string PhotoTitle 53 | { 54 | get => _photoTitle; 55 | set => SetProperty(ref _photoTitle, value, async () => await UpdateCanExecute().ConfigureAwait(false)); 56 | } 57 | 58 | public Xamarin.Forms.ImageSource? PhotoImageSource 59 | { 60 | get => _photoImageSource; 61 | set => SetProperty(ref _photoImageSource, value, async () => await UpdateCanExecute().ConfigureAwait(false)); 62 | } 63 | 64 | async Task ExecuteSavePhotoCommand(FileResult? photoMediaFile, string photoTitle) 65 | { 66 | if (IsPhotoSaving) 67 | return; 68 | 69 | if (string.IsNullOrWhiteSpace(photoTitle)) 70 | { 71 | OnSavePhotoFailed("Title Cannot Be Empty"); 72 | return; 73 | } 74 | 75 | if (photoMediaFile is null) 76 | { 77 | OnSavePhotoFailed("Photo Cannot Be Empty"); 78 | return; 79 | } 80 | 81 | IsPhotoSaving = true; 82 | 83 | try 84 | { 85 | var photoModel = await APIService.PostPhotoBlob(photoTitle, photoMediaFile).ConfigureAwait(false); 86 | 87 | await PhotoDatabase.SavePhoto(photoModel).ConfigureAwait(false); 88 | 89 | OnSavePhotoCompleted(); 90 | } 91 | catch (Exception e) 92 | { 93 | OnSavePhotoFailed(e.Message); 94 | } 95 | finally 96 | { 97 | IsPhotoSaving = false; 98 | } 99 | } 100 | 101 | async Task ExecuteTakePhotoCommand() 102 | { 103 | var mediaFile = await GetMediaFileFromCamera().ConfigureAwait(false); 104 | 105 | if (mediaFile != null) 106 | { 107 | _photoMediaFile = mediaFile; 108 | 109 | var fileStream = await mediaFile.OpenReadAsync().ConfigureAwait(false); 110 | UpdatePhotoImageSource(fileStream); 111 | } 112 | } 113 | 114 | async Task GetMediaFileFromCamera() 115 | { 116 | var arePermissionsGranted = await ArePermissionsGranted().ConfigureAwait(false); 117 | 118 | if (!arePermissionsGranted) 119 | { 120 | OnNoCameraFound(); 121 | return null; 122 | } 123 | 124 | return await MainThread.InvokeOnMainThreadAsync(() => MediaPicker.CapturePhotoAsync()).ConfigureAwait(false); 125 | } 126 | 127 | Task UpdateCanExecute() => MainThread.InvokeOnMainThreadAsync(() => 128 | { 129 | SavePhotoCommand.RaiseCanExecuteChanged(); 130 | TakePhotoCommand.RaiseCanExecuteChanged(); 131 | }); 132 | 133 | Task ArePermissionsGranted() => MainThread.InvokeOnMainThreadAsync(async () => 134 | { 135 | var cameraStatusRequestTask = Permissions.RequestAsync(); 136 | var storageWriteStatusRequestTask = Permissions.RequestAsync(); 137 | var storageReadStatusRequestTask = Permissions.RequestAsync(); 138 | var photosPermissionRequestTask = Permissions.RequestAsync(); 139 | 140 | await Task.WhenAll(cameraStatusRequestTask, storageWriteStatusRequestTask, storageReadStatusRequestTask, photosPermissionRequestTask).ConfigureAwait(false); 141 | 142 | var cameraStatus = await cameraStatusRequestTask.ConfigureAwait(false); 143 | var storageWriteStatus = await storageWriteStatusRequestTask.ConfigureAwait(false); 144 | var storageReadStatus = await storageReadStatusRequestTask.ConfigureAwait(false); 145 | var photosPermission = await photosPermissionRequestTask.ConfigureAwait(false); 146 | 147 | return cameraStatus is PermissionStatus.Granted 148 | && storageWriteStatus is PermissionStatus.Granted 149 | && storageReadStatus is PermissionStatus.Granted 150 | && photosPermission is PermissionStatus.Granted; 151 | }); 152 | 153 | void UpdatePhotoImageSource(Stream fileStream) => PhotoImageSource = Xamarin.Forms.ImageSource.FromStream(() => fileStream); 154 | 155 | void OnNoCameraFound() => _noCameraFoundEventManager.RaiseEvent(this, EventArgs.Empty, nameof(NoCameraFound)); 156 | void OnSavePhotoCompleted() => _savePhotoCompletedEventManager.RaiseEvent(this, EventArgs.Empty, nameof(SavePhotoCompleted)); 157 | void OnSavePhotoFailed(in string errorMessage) => _savePhotoFailedEventManager.RaiseEvent(this, errorMessage, nameof(SavePhotoFailed)); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/ViewModels/BaseViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Collections.Generic; 4 | using System.Runtime.CompilerServices; 5 | 6 | using AsyncAwaitBestPractices; 7 | 8 | namespace AzureBlobStorageSampleApp 9 | { 10 | public abstract class BaseViewModel : INotifyPropertyChanged 11 | { 12 | readonly WeakEventManager _notifyPropertyChangedEventManager = new WeakEventManager(); 13 | 14 | event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged 15 | { 16 | add => _notifyPropertyChangedEventManager.AddEventHandler(value); 17 | remove => _notifyPropertyChangedEventManager.RemoveEventHandler(value); 18 | } 19 | 20 | protected void SetProperty(ref T backingStore, in T value, in Action? onChanged = null, [CallerMemberName] in string propertyname = "") 21 | { 22 | if (EqualityComparer.Default.Equals(backingStore, value)) 23 | return; 24 | 25 | backingStore = value; 26 | 27 | onChanged?.Invoke(); 28 | 29 | OnPropertyChanged(propertyname); 30 | } 31 | 32 | protected void OnPropertyChanged([CallerMemberName] in string propertyName = "") => 33 | _notifyPropertyChangedEventManager.RaiseEvent(this, new PropertyChangedEventArgs(propertyName), nameof(INotifyPropertyChanged.PropertyChanged)); 34 | } 35 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/ViewModels/PhotoDetailsViewModel.cs: -------------------------------------------------------------------------------- 1 | using Xamarin.Forms; 2 | 3 | using AzureBlobStorageSampleApp.Shared; 4 | 5 | namespace AzureBlobStorageSampleApp 6 | { 7 | public class PhotoDetailsViewModel : BaseViewModel 8 | { 9 | Command? _setPhotoCommand; 10 | PhotoModel? _photo; 11 | 12 | public Command SetPhotoCommand => _setPhotoCommand ??= new Command(photo => Photo = photo); 13 | 14 | public ImageSource PhotoImageSource => ImageSource.FromUri(new System.Uri(Photo?.Url ?? string.Empty)); 15 | public string PhotoTitle => Photo?.Title ?? string.Empty; 16 | 17 | PhotoModel? Photo 18 | { 19 | get => _photo; 20 | set 21 | { 22 | _photo = value; 23 | NotifyPhotoProperties(); 24 | } 25 | } 26 | 27 | void NotifyPhotoProperties() 28 | { 29 | OnPropertyChanged(nameof(PhotoImageSource)); 30 | OnPropertyChanged(nameof(PhotoTitle)); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/ViewModels/PhotoListViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using System.Windows.Input; 7 | using AsyncAwaitBestPractices; 8 | using AsyncAwaitBestPractices.MVVM; 9 | using AzureBlobStorageSampleApp.Shared; 10 | using Xamarin.Forms; 11 | 12 | namespace AzureBlobStorageSampleApp 13 | { 14 | public class PhotoListViewModel : BaseViewModel 15 | { 16 | readonly WeakEventManager _refreshFailedEventManager = new WeakEventManager(); 17 | 18 | bool _isRefreshing; 19 | ICommand? _refreshCommand; 20 | 21 | public PhotoListViewModel() 22 | { 23 | //https://codetraveler.io/2019/09/11/using-observablecollection-in-a-multi-threaded-xamarin-forms-application/ 24 | BindingBase.EnableCollectionSynchronization(AllPhotosList, null, ObservableCollectionCallback); 25 | } 26 | 27 | public ObservableCollection AllPhotosList { get; } = new ObservableCollection(); 28 | public ICommand RefreshCommand => _refreshCommand ??= new AsyncCommand(ExecuteRefreshCommand); 29 | 30 | public event EventHandler RefreshFailed 31 | { 32 | add => _refreshFailedEventManager.AddEventHandler(value); 33 | remove => _refreshFailedEventManager.RemoveEventHandler(value); 34 | } 35 | 36 | public bool IsRefreshing 37 | { 38 | get => _isRefreshing; 39 | set => SetProperty(ref _isRefreshing, value); 40 | } 41 | 42 | async Task ExecuteRefreshCommand() 43 | { 44 | try 45 | { 46 | AllPhotosList.Clear(); 47 | 48 | await DatabaseSyncService.SyncRemoteAndLocalDatabases().ConfigureAwait(false); 49 | 50 | var unsortedPhotosList = await PhotoDatabase.GetAllPhotos().ConfigureAwait(false); 51 | 52 | foreach (var photo in unsortedPhotosList.OrderBy(x => x.Title)) 53 | { 54 | AllPhotosList.Add(photo); 55 | 56 | //Pause briefly after each photo is added to allow the UI to show the incoming cascading photos 57 | await Task.Delay(100).ConfigureAwait(false); 58 | } 59 | } 60 | catch (Exception e) 61 | { 62 | DebugServices.Log(e); 63 | OnRefreshFailedEventManager(e); 64 | } 65 | finally 66 | { 67 | IsRefreshing = false; 68 | } 69 | } 70 | 71 | //https://codetraveler.io/2019/09/11/using-observablecollection-in-a-multi-threaded-xamarin-forms-application/ 72 | void ObservableCollectionCallback(IEnumerable collection, object context, Action accessMethod, bool writeAccess) 73 | { 74 | lock(collection) 75 | { 76 | accessMethod?.Invoke(); 77 | } 78 | } 79 | 80 | void OnRefreshFailedEventManager(Exception exception) => _refreshFailedEventManager.RaiseEvent(this, exception, nameof(RefreshFailed)); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Views/MarkupExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using Xamarin.Forms; 4 | using Bounds = System.Linq.Expressions.Expression>; 5 | using Expression = System.Linq.Expressions.Expression>; 6 | 7 | namespace AzureBlobStorageSampleApp 8 | { 9 | public static class ElementExtensions 10 | { 11 | public static TElement DynamicResource(this TElement element, BindableProperty property, string key) where TElement : Element 12 | { 13 | element.SetDynamicResource(property, key); 14 | return element; 15 | } 16 | 17 | public static TElement DynamicResources(this TElement element, params (BindableProperty property, string key)[] resources) where TElement : Element 18 | { 19 | foreach (var (property, key) in resources) 20 | element.SetDynamicResource(property, key); 21 | 22 | return element; 23 | } 24 | 25 | public static TButton Padding(this TButton button, double horizontalSize, double verticalSize) where TButton : Button 26 | { 27 | button.Padding = new Thickness(horizontalSize, verticalSize); 28 | return button; 29 | } 30 | } 31 | 32 | static class MarkupExtensions 33 | { 34 | public static GridLength StarGridLength(double value) => new GridLength(value, GridUnitType.Star); 35 | public static GridLength StarGridLength(int value) => StarGridLength((double)value); 36 | 37 | public static GridLength AbsoluteGridLength(double value) => new GridLength(value, GridUnitType.Absolute); 38 | public static GridLength AbsoluteGridLength(int value) => AbsoluteGridLength((double)value); 39 | 40 | public static double GetWidth(this View view, in RelativeLayout parent) => view.Measure(parent.Width, parent.Height).Request.Width; 41 | public static double GetHeight(this View view, in RelativeLayout parent) => view.Measure(parent.Width, parent.Height).Request.Height; 42 | 43 | public static bool IsNullOrEmpty(this IEnumerable? enumerable) => !enumerable?.GetEnumerator().MoveNext() ?? true; 44 | 45 | public static RelativeLayout RelativeLayout(params ConstrainedView?[] constrainedViews) 46 | { 47 | var layout = new RelativeLayout(); 48 | foreach (var constrainedView in constrainedViews) 49 | constrainedView?.AddTo(layout); 50 | 51 | return layout; 52 | } 53 | 54 | public static ConstrainedView Unconstrained(this TView view) where TView : View => ConstrainedView.FromView(view); 55 | 56 | public static ConstrainedView Constrain(this TView view, Bounds bounds) where TView : View => ConstrainedView.FromBounds(view, bounds); 57 | 58 | public static ConstrainedView Constrain(this TView view, Expression? x = null, Expression? y = null, Expression? width = null, Expression? height = null) where TView : View => ConstrainedView.FromExpressions(view, x, y, width, height); 59 | 60 | public static ConstrainedView Constrain(this TView view, Constraint? xConstraint = null, Constraint? yConstraint = null, Constraint? widthConstraint = null, Constraint? heightConstraint = null) where TView : View => ConstrainedView.FromConstraints(view, xConstraint, yConstraint, widthConstraint, heightConstraint); 61 | 62 | public static RelativeLayout Add(this RelativeLayout relativeLayout, TView view) where TView : View? 63 | { 64 | if (view != null) 65 | ((Layout)relativeLayout).Children.Add(view); 66 | 67 | return relativeLayout; 68 | } 69 | 70 | public static RelativeLayout Add(this RelativeLayout relativeLayout, TView view, Bounds bounds) where TView : View? 71 | { 72 | if (view != null) 73 | relativeLayout.Children.Add(view, bounds); 74 | 75 | return relativeLayout; 76 | } 77 | 78 | public static RelativeLayout Add(this RelativeLayout relativeLayout, TView view, Expression? x = null, Expression? y = null, Expression? width = null, Expression? height = null) where TView : View? 79 | { 80 | if (view != null) 81 | relativeLayout.Children.Add(view, x, y, width, height); 82 | 83 | return relativeLayout; 84 | } 85 | 86 | public static RelativeLayout Add(this RelativeLayout relativeLayout, TView view, Constraint? xConstraint = null, Constraint? yConstraint = null, Constraint? widthConstraint = null, Constraint? heightConstraint = null) where TView : View? 87 | { 88 | if(view != null) 89 | relativeLayout.Children.Add(view, xConstraint, yConstraint, widthConstraint, heightConstraint); 90 | 91 | return relativeLayout; 92 | } 93 | } 94 | 95 | class ConstrainedView 96 | { 97 | readonly View _view; 98 | readonly Kind _kind; 99 | readonly Bounds? _bounds; 100 | readonly Expression? _x, _y, _width, _height; 101 | readonly Constraint? _xConstraint, _yConstraint, _widthConstraint, _heightConstraint; 102 | 103 | ConstrainedView(Kind kind, View view) => (_kind, _view) = (kind, view); 104 | 105 | ConstrainedView(Kind kind, View view, Bounds bounds) : this(kind, view) => _bounds = bounds; 106 | 107 | ConstrainedView(Kind kind, View view, Expression? x, Expression? y, Expression? width, Expression? height) 108 | : this(kind, view) => (_x, _y, _width, _height) = (x, y, width, height); 109 | 110 | ConstrainedView(Kind kind, View view, Constraint? xConstraint, Constraint? yConstraint, Constraint? widthConstraint, Constraint? heightConstraint) 111 | : this(kind, view) => (_xConstraint, _yConstraint, _widthConstraint, _heightConstraint) = (xConstraint, yConstraint, widthConstraint, heightConstraint); 112 | 113 | enum Kind { None, Bounds, Expressions, Constraints } 114 | 115 | internal void AddTo(RelativeLayout layout) 116 | { 117 | switch (_kind) 118 | { 119 | case Kind.None: 120 | ((Layout)layout).Children.Add(_view); 121 | break; 122 | 123 | case Kind.Bounds: 124 | layout.Children.Add(_view, _bounds); 125 | break; 126 | 127 | case Kind.Expressions: 128 | layout.Children.Add(_view, _x, _y, _width, _height); 129 | break; 130 | 131 | case Kind.Constraints: 132 | layout.Children.Add(_view, _xConstraint, _yConstraint, _widthConstraint, _heightConstraint); 133 | break; 134 | 135 | default: 136 | throw new NotSupportedException(); 137 | } 138 | } 139 | 140 | internal static ConstrainedView FromView(View view) => new ConstrainedView(Kind.None, view); 141 | 142 | internal static ConstrainedView FromBounds(View view, Bounds bounds) => new ConstrainedView(Kind.Bounds, view, bounds); 143 | 144 | internal static ConstrainedView FromExpressions(View view, Expression? x, Expression? y, Expression? width, Expression? height) => new ConstrainedView(Kind.Expressions, view, x, y, width, height); 145 | 146 | internal static ConstrainedView FromConstraints(View view, Constraint? xConstraint, Constraint? yConstraint, Constraint? widthConstraint, Constraint? heightConstraint) => new ConstrainedView(Kind.Constraints, view, xConstraint, yConstraint, widthConstraint, heightConstraint); 147 | } 148 | } -------------------------------------------------------------------------------- /AzureBlobStorageSampleApp/Views/PhotoList/PhotoDataTemplate.cs: -------------------------------------------------------------------------------- 1 | using AzureBlobStorageSampleApp.Shared; 2 | using Xamarin.Forms; 3 | using Xamarin.CommunityToolkit.Markup; 4 | using static AzureBlobStorageSampleApp.MarkupExtensions; 5 | using static Xamarin.CommunityToolkit.Markup.GridRowsColumns; 6 | 7 | namespace AzureBlobStorageSampleApp 8 | { 9 | public class PhotoDataTemplate : DataTemplate 10 | { 11 | public PhotoDataTemplate() : base(CreatePhotoDataTemplate) 12 | { 13 | } 14 | 15 | static Grid CreatePhotoDataTemplate() => new Grid 16 | { 17 | RowDefinitions = Rows.Define(AbsoluteGridLength(50)), 18 | 19 | ColumnDefinitions = Columns.Define( 20 | (Column.Image, AbsoluteGridLength(50)), 21 | (Column.Title, Star)), 22 | 23 | Children = 24 | { 25 | new Image { BackgroundColor = ColorConstants.PageBackgroundColor }.Center().Margin(0, 5) 26 | .Column(Column.Image) 27 | .Bind(Image.SourceProperty, nameof(PhotoModel.Url)), 28 | 29 | new Label().TextCenterVertical() 30 | .Column(Column.Title) 31 | .Bind(Label.TextProperty, nameof(PhotoModel.Title)) 32 | } 33 | }.Padding(10, 0); 34 | 35 | enum Column { Image, Title } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | latest 5 | enable 6 | nullable 7 | True 8 | false 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AzureBlobStorageSampleApp 2 | 3 | This Xamarin app uses a [SQLite Database](https://github.com/praeclarum/sqlite-net) to save the metadata of the Photos (e.g. Url, Title) locally. The local database syncs, via an [Azure Function](https://aka.ms/XamarinBlog/AzureFunctions), with an [Azure SQL Database](https://aka.ms/XamarinBlog/AzureSQL) that contains the metadata of the Photos stored in [Azure Blob Storage](https://aka.ms/xamarinblog/azureblobstorage). 4 | 5 | The Xamarin app also allows the user to take photos and save them to [Azure Blob Storage](https://aka.ms/xamarinblog/azureblobstorage). To do this, the Xamarin app uploads the image to an [Azure Function](https://aka.ms/XamarinBlog/AzureFunctions), and the [Azure Function](https://aka.ms/XamarinBlog/AzureFunctions) saves the image in [Azure Blob Storage](https://aka.ms/xamarinblog/azureblobstorage), then adds the image metadata to the [Azure SQL Database](https://aka.ms/XamarinBlog/AzureSQL). 6 | 7 | ## Learn More 8 | - [The Xamarin Show: Azure Blob Storage for Mobile](https://channel9.msdn.com/Shows/XamarinShow/Azure-Blob-Storage-for-Mobile-with-Brandon-Minnick/?WT.mc_id=mobile-0000-bramin) 9 | - [Xamarin Blog: Add Cloud Storage to Xamarin Apps with Azure Blob Storage](https://blog.xamarin.com/xamarin-plus-azure-blob-cloud-storage/?WT.mc_id=none-ch9-bramin) 10 | - [Azure Blob Storage](https://aka.ms/xamarinblog/azureblobstorage) 11 | - [How to use Blob Storage from Xamarin](https://aka.ms/XamarinBlog/AzureBlobStorageWithXamarin) 12 | 13 | ![Azure Blob Storage Sample App Diagram](https://github.com/brminnick/Videos/blob/master/AzureBlobStorageSampleApp/AzureBlobStorageSampleAppDiagram.png) 14 | 15 | ## Getting Started 16 | 17 | ### 1. Publish The Function App to Azure 18 | 19 | ![](https://user-images.githubusercontent.com/3628580/52371778-68fb4e00-2a24-11e9-9617-bc9d6580bd8f.png) 20 | 21 | 1. In Visual Studio, right-click on AzureBlobStorageSampleApp.Functions and select Publish 22 | 23 | ![](https://user-images.githubusercontent.com/3628580/52371889-af50ad00-2a24-11e9-8c78-f3c8b5141566.png) 24 | 25 | 2. Choose Azure Function App -> Create New -> Publish 26 | 27 | ![](https://user-images.githubusercontent.com/3628580/52372329-eb384200-2a25-11e9-9833-d26397201523.png) 28 | 29 | 3. Fill out details: 30 | - App Name: Pick a name for your app within Azure 31 | - Subscription: Select your Azure subscription 32 | - Resource Group: Pick a resource group or create a new one 33 | - Hosting Plan: Pick a name, a region close to you, and for Size I chose Consumption 34 | - Storage Account: Create a new one. Pick a name, for Account Type I chose: Standard - Locally Redudant Storage 35 | 36 | 37 | 3. Click Create 38 | 39 | 4. This will take a couple minutes to deploy; confirm its existance in the Azure portal. We're done with the Function for now, but we'll be back to grab a couple values and add a couple values to the Application Settings. 40 | 41 | ### 2. Create Azure SQL Database 42 | 43 | ![](https://user-images.githubusercontent.com/13558917/29196780-9324ac1c-7deb-11e7-9d87-8a95ab62b0c5.png) 44 | 45 | 1. In the Azure portal, click on New -> Enter `SQL Database` into the Search Bar -> Select `SQL Database` from the search results -> Click Create 46 | 47 | ![](https://user-images.githubusercontent.com/13558917/29197883-2b850292-7df4-11e7-8bfd-8016d72f799a.png) 48 | 49 | 2. Name the SQL Database 50 | - I named mine XamListDatabase 51 | 3. Select the Subscription 52 | - I selected my Visual Studio Enterprise subscription 53 | - If you do not have a VS Enterprise Subscription, you will need to select a different option 54 | 4. Select the Resource Group you published your Function in 55 | 5. Select Blank Database 56 | 57 | ![](https://user-images.githubusercontent.com/13558917/29198124-efa3b08c-7df5-11e7-87f4-42cf0dc95862.png) 58 | 6. Select Server 59 | 60 | 7. Select Create New Server 61 | 62 | 8. Enter the Server Name 63 | 64 | 9. Create a Server admin login 65 | - Store this password somewhere safe, because we will need to use it for our database connection later! 66 | 67 | 10. Create a password 68 | 69 | 11. Select the closest location 70 | 71 | 12. Click "Select" 72 | 73 | 13. Select "Not Now" for the SQL Elastic Pool option 74 | 75 | ![](https://user-images.githubusercontent.com/13558917/29198240-f8b25cae-7df6-11e7-8f76-b8977645a712.png) 76 | 14. Select Pricing Tier 77 | 1. Select Basic 78 | 2. Move the slider to maximum, 2GB 79 | - Don't worry, it's the same price for 2GB as it is for 100MB. 80 | 3. Click Apply 81 | 15. Click Create 82 | 83 | ### 3. Get SQL Database Connection String 84 | 85 | ![](https://user-images.githubusercontent.com/13558917/29198409-9d0dcab2-7df8-11e7-8c41-4797228ee4ab.png) 86 | 87 | 1. On the Azure Portal, navigate to the SQL Database we created, above 88 | 2. Click on "Connection Strings" -> "ADO.NET" 89 | 3. Copy the entire Connection String into a text editor 90 | 91 | ![](https://user-images.githubusercontent.com/13558917/29198528-b26f19f0-7df9-11e7-82c2-b4d46f60389a.png) 92 | 93 | 4. In the text editor, change "{your_username}" and "{your_password}" to match the SQL Database Username / Password created above 94 | - Don't use my username / password because it won't work ;-) 95 | 96 | ### 4. Connect SQL Database to the Azure Function App 97 | 98 | ![](https://user-images.githubusercontent.com/13558917/29198794-f3673e5e-7dfb-11e7-89fc-ee042fe34704.png) 99 | 100 | 1. On the Azure Portal, navigate to the Functions App we published from Visual Studio 101 | 2. Select "Application Settings" 102 | 3. In the Application Settings, scroll down to the section "Application Settings" 103 | 4. Create a new string 104 | - Set the name as `PhotoDatabaseConnectionString` 105 | - Make sure to use this _exact_ name, otherwise the source code will not work 106 | - Copy/paste the Azure SQL connection string from the text editor as the corresponding value 107 | 5. Scroll up and click Save (Note! If you don't click Save - the change will not be reflected.) 108 | 109 | ### 5. Create a (Blob) Storage Account 110 | 111 | ![](https://user-images.githubusercontent.com/3628580/52376722-d57c4a00-2a30-11e9-8c09-1374df8de3db.png) 112 | 113 | 1. In the Azure portal, click on New -> Enter `Storage account` into the Search Bar -> Select `Storage acount` from the search results -> Click Create 114 | 115 | ![](https://user-images.githubusercontent.com/3628580/52378151-f050bd80-2a34-11e9-8914-d75f204da6e2.png) 116 | 117 | 2. In the next screen, you'll enter a few values 118 | - Select the Subscription and the Resource Group you have been working in 119 | - Create a Storage account name (note: letters in the name need to be lowercase) 120 | - Choose a location 121 | - For Performance, I chose Standard 122 | - For Account king, I chose StorageV2 (general purchase v2) 123 | - For Access tier, chose Hot 124 | 125 | 3. Click Review and Create 126 | 127 | ### 6. Create a Blob container 128 | 1. Click into the Storage account you created. 129 | 130 | ![image](https://user-images.githubusercontent.com/3628580/52377203-2cceea00-2a32-11e9-840c-4cdf66c7d90f.png) 131 | 132 | 2. On the left menu, under Blob service, click Blobs 133 | 134 | 3. Click the "+ Container" button to create a new container 135 | 136 | 4. Use "photos" for the Name, and for the purposes of this exercise, chose Public access level: Container (anonymous read access for containers and blob) 137 | - In future apps, you'll likely want to increase the privacy of your blob containers 138 | 139 | ### 7. Connect the (Blob) Storage Account to the Azure Function App 140 | 141 | ![](https://user-images.githubusercontent.com/3628580/52377592-5f2d1700-2a33-11e9-90e1-7703890ce69a.png) 142 | 143 | 1. In the Storage Account, click Access Keys which are under Settings 144 | 145 | 2. You'll see key 1 and key 2 along with a Key and Connection String for each of those. Copy either of the Connection Strings. 146 | 147 | ![](https://user-images.githubusercontent.com/13558917/29198794-f3673e5e-7dfb-11e7-89fc-ee042fe34704.png) 148 | 149 | 1. On the Azure Portal, navigate to the Functions App we published from Visual Studio 150 | 151 | 2. Select "Application Settings" 152 | 153 | 3. In the Application Settings, scroll down to "Application Settings" 154 | 155 | 4. Create a new setting 156 | - Set the name as `BlobStorageConnectionString` 157 | - Make sure to use this _exact_ name, otherwise the source code will not work 158 | - Copy/paste the Connection setting 159 | 160 | 5. Create another setting 161 | - Set the name as `PhotoContainerName` 162 | - Make sure to use this _exact_ name, otherwise the source code will not work 163 | - Type `photos` as the corresponding value (This is the name of the container you created earlier.) 164 | 165 | 6. Scroll up and click Save (Note! If you don't click Save - the change will not be reflected.) 166 | 167 | ### 8. Configure Azure Function Url & Keys for Mobile App 168 | 169 | ![](https://user-images.githubusercontent.com/3628580/52378689-5c7ff100-2a36-11e9-81ff-caf04aa70767.png) 170 | 171 | 1. In [BackendConstants.cs](https://github.com/brminnick/AzureBlobStorageSampleApp/blob/master/AzureBlobStorageSampleApp.Mobile.Shared/Constants/BackendConstants.cs), you'll need to customize the value of `FunctionsAPIBaseUrl` to match yours 172 | - Notice the URL in the upper right of the photo. You'll only need to change the subdomain to match yours (ie. it is important that the URL in code retains the `/api`). 173 | --------------------------------------------------------------------------------