├── .gitignore
├── Automation
└── Bitrise.io_configs
│ └── app-customers_master_BuildAndDeploy.yml
├── Customers.UITests
├── AppInitializer.cs
├── Customers.UITests.csproj
├── Tests
│ ├── AndroidTests.cs
│ └── iOSTests.cs
└── packages.config
├── Customers.sln
├── Customers
├── App.xaml
├── App.xaml.cs
├── Constants
│ └── MessageKeys.cs
├── Converters
│ └── BooleanInverter.cs
├── Customers.csproj
├── Data
│ ├── Customer.cs
│ ├── CustomerDataSource.cs
│ ├── IDataSource.cs
│ └── ILocalization.cs
├── Extensions
│ ├── IntExtensions.cs
│ └── StringExtensions.cs
├── Pages
│ ├── CustomerDetailPage.xaml
│ ├── CustomerDetailPage.xaml.cs
│ ├── CustomerEditPage.xaml
│ ├── CustomerEditPage.xaml.cs
│ ├── CustomerListPage.xaml
│ └── CustomerListPage.xaml.cs
├── Properties
│ └── AssemblyInfo.cs
├── Services
│ ├── CapabilityService.cs
│ ├── ICapabilityService.cs
│ └── IEnvironmentService.cs
├── ViewModels
│ ├── BaseNavigationViewModel.cs
│ ├── CustomerDetailViewModel.cs
│ └── CustomerListViewModel.cs
├── Views
│ └── FloatingActionButtonView.cs
└── packages.config
├── Droid
├── Assets
│ └── AboutAssets.txt
├── Customers.Droid.csproj
├── Localization.cs
├── MainActivity.cs
├── Properties
│ ├── AndroidManifest.xml
│ └── AssemblyInfo.cs
├── Renderers
│ └── FloatingActionButtonViewRenderer.cs
├── Resources
│ ├── AboutResources.txt
│ ├── Resource.designer.cs
│ ├── drawable-hdpi
│ │ ├── directions.png
│ │ ├── edit.png
│ │ ├── email.png
│ │ ├── fab_add.png
│ │ ├── icon.png
│ │ ├── message.png
│ │ ├── phone.png
│ │ ├── placeholderProfileImage.png
│ │ └── save.png
│ ├── drawable-ldpi
│ │ ├── directions.png
│ │ ├── edit.png
│ │ ├── email.png
│ │ ├── fab_add.png
│ │ ├── icon.png
│ │ ├── message.png
│ │ ├── phone.png
│ │ ├── placeholderProfileImage.png
│ │ └── save.png
│ ├── drawable-mdpi
│ │ ├── directions.png
│ │ ├── edit.png
│ │ ├── email.png
│ │ ├── fab_add.png
│ │ ├── icon.png
│ │ ├── message.png
│ │ ├── phone.png
│ │ ├── placeholderProfileImage.png
│ │ └── save.png
│ ├── drawable-xhdpi
│ │ ├── directions.png
│ │ ├── edit.png
│ │ ├── email.png
│ │ ├── fab_add.png
│ │ ├── icon.png
│ │ ├── message.png
│ │ ├── phone.png
│ │ ├── placeholderProfileImage.png
│ │ └── save.png
│ ├── drawable-xxhdpi
│ │ ├── directions.png
│ │ ├── edit.png
│ │ ├── email.png
│ │ ├── fab_add.png
│ │ ├── icon.png
│ │ ├── message.png
│ │ ├── phone.png
│ │ ├── placeholderProfileImage.png
│ │ └── save.png
│ ├── drawable-xxxhdpi
│ │ ├── directions.png
│ │ ├── edit.png
│ │ ├── email.png
│ │ ├── fab_add.png
│ │ ├── icon.png
│ │ ├── message.png
│ │ ├── phone.png
│ │ ├── placeholderProfileImage.png
│ │ └── save.png
│ ├── drawable
│ │ ├── directions.png
│ │ ├── edit.png
│ │ ├── email.png
│ │ ├── fab_add.png
│ │ ├── icon.png
│ │ ├── message.png
│ │ ├── phone.png
│ │ ├── placeholderProfileImage.png
│ │ └── save.png
│ ├── layout
│ │ └── toolbar.axml
│ ├── values-v21
│ │ └── styles.xml
│ └── values
│ │ ├── colors.xml
│ │ └── styles.xml
├── Services
│ └── EnvironmentService.cs
├── googleMapsApiKeyUtil.sh
└── packages.config
├── LICENSE
├── README.md
├── Screenshots
├── Customers_DetailPage.png
├── Customers_EditPage.png
├── Customers_GetDirections.png
├── Customers_ListPage.png
└── Customers_Screens.jpg
└── iOS
├── AppDelegate.cs
├── Customers.iOS.csproj
├── Entitlements.plist
├── Info.plist
├── Localization.cs
├── Main.cs
├── Renderers
└── StandardViewCellRenderer.cs
├── Resources
├── Images.xcassets
│ └── AppIcons.appiconset
│ │ └── Contents.json
├── LaunchScreen.xib
├── add.png
├── add@2x.png
├── add@3x.png
├── directions.png
├── directions@2x.png
├── directions@3x.png
├── edit.png
├── edit@2x.png
├── edit@3x.png
├── email.png
├── email@2x.png
├── email@3x.png
├── message.png
├── message@2x.png
├── message@3x.png
├── phone.png
├── phone@2x.png
├── phone@3x.png
├── placeholderProfileImage.png
├── save.png
├── save@2x.png
└── save@3x.png
├── Services
└── EnvironmentService.cs
└── packages.config
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.sln.docstates
8 | *.userprefs
9 |
10 | # Xamarin Components
11 | Components/
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | x64/
18 | build/
19 | bld/
20 | [Bb]in/
21 | [Oo]bj/
22 | [Pp]ackages/
23 |
24 | # MSTest test Results
25 | [Tt]est[Rr]esult*/
26 | [Bb]uild[Ll]og.*
27 |
28 | #NUNIT
29 | *.VisualState.xml
30 | TestResult.xml
31 |
32 | # Build Results of an ATL Project
33 | [Dd]ebugPS/
34 | [Rr]eleasePS/
35 | dlldata.c
36 |
37 | *_i.c
38 | *_p.c
39 | *_i.h
40 | *.ilk
41 | *.meta
42 | *.obj
43 | *.pch
44 | *.pdb
45 | *.pgc
46 | *.pgd
47 | *.rsp
48 | *.sbr
49 | *.tlb
50 | *.tli
51 | *.tlh
52 | *.tmp
53 | *.tmp_proj
54 | *.log
55 | *.vspscc
56 | *.vssscc
57 | .builds
58 | *.pidb
59 | *.svclog
60 | *.scc
61 |
62 | # Chutzpah Test files
63 | _Chutzpah*
64 |
65 | # Visual C++ cache files
66 | ipch/
67 | *.aps
68 | *.ncb
69 | *.opensdf
70 | *.sdf
71 | *.cachefile
72 |
73 | # Visual Studio profiler
74 | *.psess
75 | *.vsp
76 | *.vspx
77 |
78 | # TFS 2012 Local Workspace
79 | $tf/
80 |
81 | # Guidance Automation Toolkit
82 | *.gpState
83 |
84 | # ReSharper is a .NET coding add-in
85 | _ReSharper*/
86 | *.[Rr]e[Ss]harper
87 | *.DotSettings.user
88 |
89 | # JustCode is a .NET coding addin-in
90 | .JustCode
91 |
92 | # TeamCity is a build add-in
93 | _TeamCity*
94 |
95 | # DotCover is a Code Coverage Tool
96 | *.dotCover
97 |
98 | # NCrunch
99 | *.ncrunch*
100 | _NCrunch_*
101 | .*crunch*.local.xml
102 |
103 | # MightyMoose
104 | *.mm.*
105 | AutoTest.Net/
106 |
107 | # Web workbench (sass)
108 | .sass-cache/
109 |
110 | # Installshield output folder
111 | [Ee]xpress/
112 |
113 | # DocProject is a documentation generator add-in
114 | DocProject/buildhelp/
115 | DocProject/Help/*.HxT
116 | DocProject/Help/*.HxC
117 | DocProject/Help/*.hhc
118 | DocProject/Help/*.hhk
119 | DocProject/Help/*.hhp
120 | DocProject/Help/Html2
121 | DocProject/Help/html
122 |
123 | # Click-Once directory
124 | publish/
125 |
126 | # Publish Web Output
127 | *.[Pp]ublish.xml
128 | *.azurePubxml
129 |
130 | # Windows Azure Build Output
131 | csx/
132 | *.build.csdef
133 |
134 | # Windows Store app package directory
135 | AppPackages/
136 |
137 | # Others
138 | sql/
139 | *.Cache
140 | ClientBin/
141 | [Ss]tyle[Cc]op.*
142 | ~$*
143 | *~
144 | *.dbmdl
145 | *.dbproj.schemaview
146 | *.pfx
147 | *.publishsettings
148 | node_modules/
149 | .DS_Store
150 |
151 | # RIA/Silverlight projects
152 | Generated_Code/
153 |
154 | # Backup & report files from converting an old project file to a newer
155 | # Visual Studio version. Backup files are not needed, because we have git ;-)
156 | _UpgradeReport_Files/
157 | Backup*/
158 | UpgradeLog*.XML
159 | UpgradeLog*.htm
160 |
161 | # SQL Server files
162 | *.mdf
163 | *.ldf
164 |
165 | # Business Intelligence projects
166 | *.rdl.data
167 | *.bim.layout
168 | *.bim_*.settings
169 |
170 | # Microsoft Fakes
171 | FakesAssemblies/
172 |
173 | *.bak
--------------------------------------------------------------------------------
/Automation/Bitrise.io_configs/app-customers_master_BuildAndDeploy.yml:
--------------------------------------------------------------------------------
1 | ---
2 | format_version: 1.1.0
3 | default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git
4 | app:
5 | envs:
6 | - opts:
7 | is_expand: true
8 | XAMARIN_SOLUTION_FILEPATH: Customers.sln
9 | - opts:
10 | is_expand: true
11 | XAMARIN_PROJECT_FOLDERPATH_IOS: iOS
12 | - opts:
13 | is_expand: true
14 | XAMARIN_PROJECT_FOLDERPATH_ANDROID: Droid
15 | - opts:
16 | is_expand: true
17 | XAMARIN_PROJECT_FILENAME_IOS: Customers.iOS.csproj
18 | - opts:
19 | is_expand: true
20 | XAMARIN_PROJECT_FILENAME_ANDROID: Customers.Droid.csproj
21 | - opts:
22 | is_expand: true
23 | XAMARIN_CONFIG_IOS: Release
24 | - opts:
25 | is_expand: true
26 | XAMARIN_CONFIG_ANDROID: Release
27 | - opts:
28 | is_expand: true
29 | XAMARIN_PLATFORM_IOS: iPhone
30 | - opts:
31 | is_expand: true
32 | XAMARIN_PLATFORM_ANDROID: AnyCPU
33 | - opts:
34 | is_expand: true
35 | XAMARIN_OUTPUT_FILENAME_IOS: Customers.ipa
36 | - opts:
37 | is_expand: true
38 | XAMARIN_OUTPUT_DSYM_UNZIPPED_FILENAME: CustomersiOS.app.dSYM
39 | - opts:
40 | is_expand: true
41 | XAMARIN_OUTPUT_DSYM_ZIPPED_FILENAME: CustomersiOS.app.dSYM.zip
42 | - opts:
43 | is_expand: true
44 | XAMARIN_OUTPUT_FILENAME_ANDROID_UNSIGNED: com.xamarin.customers.apk
45 | - opts:
46 | is_expand: true
47 | XAMARIN_OUTPUT_FILENAME_ANDROID_SIGNED: signed-com.xamarin.customers.apk
48 | - opts:
49 | is_expand: true
50 | XAMARIN_IOS_INFOPLIST_FILEPATH: "$XAMARIN_PROJECT_FOLDERPATH_IOS/Info.plist"
51 | - opts:
52 | is_expand: true
53 | XAMARIN_ANDROID_MANIFESTXML_FILEPATH: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/Properties/AndroidManifest.xml"
54 | - opts:
55 | is_expand: true
56 | XAMARIN_PROJECT_FILEPATH_IOS: "$XAMARIN_PROJECT_FOLDERPATH_IOS/$XAMARIN_PROJECT_FILENAME_IOS"
57 | - opts:
58 | is_expand: true
59 | XAMARIN_PROJECT_FILEPATH_ANDROID: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/$XAMARIN_PROJECT_FILENAME_ANDROID"
60 | - opts:
61 | is_expand: true
62 | XAMARIN_OUTPUT_FOLDERPATH_IOS: "$XAMARIN_PROJECT_FOLDERPATH_IOS/bin/$XAMARIN_PLATFORM_IOS/$XAMARIN_CONFIG_IOS"
63 | - opts:
64 | is_expand: true
65 | XAMARIN_OUTPUT_FOLDERPATH_ANDROID: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/bin/$XAMARIN_PLATFORM_ANDROID/$XAMARIN_CONFIG_ANDROID"
66 | - opts:
67 | is_expand: true
68 | XAMARIN_PROJECT_FILEPATH_ANDROID: "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/$XAMARIN_PROJECT_FILENAME_ANDROID"
69 | - opts:
70 | is_expand: true
71 | XAMARIN_IPA_PATH: "$XAMARIN_OUTPUT_FOLDERPATH_IOS/$XAMARIN_OUTPUT_FILENAME_IOS"
72 | - opts:
73 | is_expand: true
74 | XAMARIN_APK_SOURCE_PATH: "$XAMARIN_OUTPUT_FOLDERPATH_ANDROID/$XAMARIN_OUTPUT_FILENAME_ANDROID_UNSIGNED"
75 | - opts:
76 | is_expand: true
77 | XAMARIN_APK_DESTINATION_PATH: "$XAMARIN_OUTPUT_FOLDERPATH_ANDROID/$XAMARIN_OUTPUT_FILENAME_ANDROID_SIGNED"
78 | - opts:
79 | is_expand: true
80 | XAMARIN_OUTPUT_DSYM_UNZIPPED_FILEPATH: "$XAMARIN_OUTPUT_FOLDERPATH_IOS/$XAMARIN_OUTPUT_DSYM_UNZIPPED_FILENAME"
81 | - opts:
82 | is_expand: true
83 | XAMARIN_OUTPUT_DSYM_ZIPPED_FILEPATH: "$XAMARIN_OUTPUT_FOLDERPATH_IOS/$XAMARIN_OUTPUT_DSYM_ZIPPED_FILENAME"
84 | trigger_map:
85 | - pattern: master
86 | is_pull_request_allowed: false
87 | workflow: master
88 | workflows:
89 | master:
90 | steps:
91 | - activate-ssh-key:
92 | title: Activate App SSH key
93 | inputs:
94 | - ssh_key_save_path: "$HOME/.ssh/steplib_ssh_step_id_rsa"
95 | - git-clone: {}
96 | - certificate-and-profile-installer: {}
97 | - xamarin-user-management:
98 | title: Xamarin Login
99 | - nuget-restore:
100 | title: Restore NuGets
101 | inputs:
102 | - xamarin_solution: "$XAMARIN_SOLUTION_FILEPATH"
103 | - xamarin-components-restore:
104 | title: Restore Components
105 | inputs:
106 | - xamarin_solution: "$XAMARIN_SOLUTION_FILEPATH"
107 | - set-xcode-build-number@1.0.1:
108 | inputs:
109 | - plist_path: "$XAMARIN_IOS_INFOPLIST_FILEPATH"
110 | - xamarin-builder@0.9.2:
111 | title: Build iOS
112 | inputs:
113 | - xamarin_project: "$XAMARIN_PROJECT_FILEPATH_IOS"
114 | - xamarin_configuration: "$XAMARIN_CONFIG_IOS"
115 | - xamarin_platform: "$XAMARIN_PLATFORM_IOS"
116 | - is_clean_build: 'true'
117 | - script@1.1.0:
118 | title: Upload DSYMs to Xamarin Insights
119 | inputs:
120 | - content: |-
121 | #!/bin/bash
122 |
123 | zip -r $XAMARIN_OUTPUT_DSYM_ZIPPED_FILEPATH $XAMARIN_OUTPUT_DSYM_UNZIPPED_FILEPATH
124 |
125 | curl -F "dsym=@$XAMARIN_OUTPUT_DSYM_ZIPPED_FILEPATH;type=application/zip" https://xaapi.xamarin.com/api/dsym?apikey=$XAMARIN_INSIGHTS_KEY
126 | opts:
127 | is_expand: true
128 | - script:
129 | title: Inject Google Maps API key
130 | inputs:
131 | - content: |-
132 | #!/bin/bash
133 |
134 | sed -i '' "s/GOOGLE_MAPS_API_KEY/$GOOGLE_MAPS_API_KEY/g" "$XAMARIN_PROJECT_FOLDERPATH_ANDROID/Properties/AndroidManifest.xml"
135 | opts:
136 | is_expand: true
137 | - working_dir: ''
138 | opts:
139 | is_expand: false
140 | - xamarin-builder:
141 | title: Build Android
142 | inputs:
143 | - xamarin_project: "$XAMARIN_PROJECT_FILEPATH_ANDROID"
144 | - xamarin_configuration: "$XAMARIN_CONFIG_ANDROID"
145 | - xamarin_platform: "$XAMARIN_PLATFORM_ANDROID"
146 | - is_clean_build: 'false'
147 | - command: archive
148 | - sign-apk:
149 | inputs:
150 | - apk_path: "$XAMARIN_APK_SOURCE_PATH"
151 | - xamarin-user-management:
152 | title: Xamarin Logout
153 | inputs:
154 | - xamarin_action: logout
155 | - deploy-to-bitrise-io:
156 | title: Deploy IPA
157 | inputs:
158 | - deploy_path: "$XAMARIN_IPA_PATH"
159 | - deploy-to-bitrise-io:
160 | title: Deploy APK
161 | inputs:
162 | - deploy_path: "$XAMARIN_APK_DESTINATION_PATH"
163 | before_run:
164 | after_run:
165 |
--------------------------------------------------------------------------------
/Customers.UITests/AppInitializer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using Xamarin.UITest;
5 | using Xamarin.UITest.Queries;
6 |
7 | namespace Customers.UITests
8 | {
9 | public class AppInitializer
10 | {
11 | public static IApp StartApp(Platform platform)
12 | {
13 | if (platform == Platform.Android)
14 | {
15 | return ConfigureApp
16 | .Android
17 | .ApkFile ("../../../Droid/bin/Release/com.xamarin.customers-Signed.apk")
18 | .StartApp();
19 | }
20 |
21 | return ConfigureApp
22 | .iOS
23 | .AppBundle ("../../../iOS/bin/iPhoneSimulator/Debug/CustomersiOS.app")
24 | .StartApp();
25 | }
26 | }
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/Customers.UITests/Customers.UITests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}
7 | Library
8 | Customers.UITests
9 | Customers.UITests
10 | v4.5
11 |
12 |
13 | true
14 | full
15 | false
16 | bin\Debug
17 | DEBUG;
18 | prompt
19 | 4
20 | false
21 |
22 |
23 | full
24 | true
25 | bin\Release
26 | prompt
27 | 4
28 | false
29 |
30 |
31 |
32 |
33 | ..\packages\NUnit.2.6.3\lib\nunit.framework.dll
34 |
35 |
36 | ..\packages\Xamarin.UITest.1.2.0\lib\Xamarin.UITest.dll
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/Customers.UITests/Tests/AndroidTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using NUnit.Framework;
5 | using Xamarin.UITest;
6 | using Xamarin.UITest.Queries;
7 | using System.Collections.Generic;
8 |
9 | namespace Customers.UITests
10 | {
11 | [TestFixture(Platform.Android)]
12 | public class AndroidTests
13 | {
14 | IApp app;
15 | Platform platform;
16 |
17 | public AndroidTests(Platform platform)
18 | {
19 | this.platform = platform;
20 | }
21 |
22 | [SetUp]
23 | public void BeforeEachTest()
24 | {
25 | app = AppInitializer.StartApp(platform);
26 | }
27 |
28 |
29 | [Test]
30 | public void EditCustomer ()
31 | {
32 | app.Tap(x => x.Marked("Armstead, Evan"));
33 | app.Screenshot("Selected contact with name: 'Armstead, Evan'");
34 | app.Tap(x => x.Marked("Edit"));
35 | app.Screenshot("Tapped on 'edit' button");
36 | app.Tap(x => x.Marked("First").Sibling());
37 | app.ClearText(x => x.Marked("First").Sibling());
38 | app.EnterText("Edited");
39 | app.Screenshot("Entered 'Edited' into First Name Field");
40 | app.Tap(x => x.Marked("Last").Sibling());
41 | app.ClearText(x => x.Marked("Last").Sibling());
42 | app.EnterText("Name");
43 | app.Screenshot("Entered 'Name' into Last Name Field");
44 | app.Tap(x => x.Class("ActionMenuItemView"));
45 | app.Screenshot("Saved contact");
46 | app.WaitForElement(x => x.Text("Edited Name"));
47 | app.Screenshot("Verified contacts name changed to: 'Edited Name'");
48 | }
49 |
50 | [Test]
51 | public void EditCustomerInList ()
52 | {
53 | app.Tap(x => x.Marked("Cardell, Jesus"));
54 | app.Screenshot("Selected contact with name: 'Cardell, Jesus'");
55 | app.Tap(x => x.Marked("Edit"));
56 | app.Screenshot("Tapped on 'Edit' button");
57 | app.Tap(x => x.Marked("First").Sibling());
58 | app.ClearText(x => x.Marked("First").Sibling());
59 | app.EnterText("New Name");
60 | app.Screenshot("Entered 'New Name' into First Name Field");
61 | app.Tap(x => x.Marked("Last").Sibling());
62 | app.ClearText(x => x.Marked("Last").Sibling());
63 | app.EnterText("In List");
64 | app.Screenshot("Entered 'In List' into Last Name Field");
65 | app.Tap(x => x.Class("ActionMenuItemView"));
66 | app.Screenshot("Saved contact");
67 | app.Tap(x => x.Class("android.widget.ImageButton"));
68 | app.Screenshot("Tapped on 'back' button");
69 | app.ScrollDownTo(x => x.Marked("In List, New Name"));
70 | app.Screenshot("Verified edited name appears in list");
71 | }
72 |
73 | [Test]
74 | public void UpdateCustomerAddress ()
75 | {
76 | app.Tap(x => x.Marked("Boone, John"));
77 | app.Screenshot("Selected contact with name: 'Boone, John'");
78 | app.WaitForElement("Google Map");
79 | app.Screenshot("Verified map is set to current address");
80 | app.Tap(x => x.Marked("Edit"));
81 | app.Screenshot("Tapped on 'Edit' button");
82 | app.ScrollDownTo(x => x.Marked("Street").Sibling());
83 | app.Tap(x => x.Marked("Street").Sibling());
84 | app.ClearText(x => x.Marked("Street").Sibling());
85 | app.EnterText("394 Pacific Ave");
86 | app.Screenshot("Entered '394 Pacific Ave' into Street Field");
87 | app.PressEnter();
88 | app.ScrollDownTo("City");
89 | app.Tap(x => x.Marked("City").Sibling());
90 | app.ClearText(x => x.Marked("City").Sibling());
91 | app.EnterText("San Francisco");
92 | app.Screenshot("Entered 'San Francisco' into City Field");
93 | app.PressEnter();
94 | app.ScrollDownTo(x => x.Marked("State").Sibling());
95 | app.Tap(x => x.Marked("State").Sibling());
96 | app.ClearText(x => x.Marked("State").Sibling());
97 | app.EnterText("CA");
98 | app.Screenshot("Entered 'CA' into State Field");
99 | app.PressEnter();
100 | app.Tap(x => x.Marked("ZIP").Sibling());
101 | app.ClearText(x => x.Marked("ZIP").Sibling());
102 | app.EnterText("94111");
103 | app.Screenshot("Entered '94111' into ZIP Field");
104 | app.DismissKeyboard();
105 | app.Tap(x => x.Class("ActionMenuItemView"));
106 | app.Screenshot("Saved contact");
107 | app.WaitForElement("Google Map");
108 | app.Screenshot("Verified map is set to new address");
109 | }
110 |
111 | [Test]
112 | public void AddNewCustomer ()
113 | {
114 | app.Tap(x => x.Class("FloatingActionButton"));
115 | app.Screenshot("Tapped on 'add' button");
116 | app.Tap(x => x.Marked("First").Sibling());
117 | app.EnterText("NEW");
118 | app.Screenshot("Entered 'NEW' into First Name Field");
119 | app.DismissKeyboard();
120 | app.ScrollDownTo(x => x.Marked("Last").Sibling());
121 | app.Tap(x => x.Marked("Last").Sibling());
122 | app.EnterText("CONTACT");
123 | app.Screenshot("Entered 'CONTACT' into Last Name Field");
124 | app.DismissKeyboard();
125 | app.ScrollDownTo(x => x.Marked("Company").Sibling());
126 | app.Tap(x => x.Marked("Company").Sibling());
127 | app.EnterText("Xamarin");
128 | app.Screenshot("Entered 'Xamarin' into Company Field");
129 | app.DismissKeyboard();
130 | app.ScrollDownTo(x => x.Marked("Title").Sibling());
131 | app.Tap(x => x.Marked("Title").Sibling());
132 | app.EnterText("Test Cloud");
133 | app.Screenshot("Entered 'Test Cloud' into Title Field");
134 | app.DismissKeyboard();
135 | app.ScrollDownTo(x => x.Marked("Phone").Sibling());
136 | app.Tap(x => x.Marked("Phone").Sibling());
137 | app.EnterText("1234567890");
138 | app.Screenshot("Entered '1234567890' into Phone Field");
139 | app.DismissKeyboard();
140 | app.ScrollDownTo(x => x.Marked("Email").Sibling());
141 | app.Tap(x => x.Marked("Email").Sibling());
142 | app.EnterText("hello@xamarin.com");
143 | app.Screenshot("Entered 'hello@xamarin.com' into Email Field");
144 | app.DismissKeyboard();
145 | app.ScrollDownTo(x => x.Marked("Street").Sibling());
146 | app.Tap(x => x.Marked("Street").Sibling());
147 | app.EnterText("394 Pacific Ave");
148 | app.Screenshot("Entered '394 Pacific Ave' into Street Field");
149 | app.DismissKeyboard();
150 | app.ScrollDownTo(x => x.Marked("City").Sibling());
151 | app.Tap(x => x.Marked("City").Sibling());
152 | app.EnterText("San Francisco");
153 | app.Screenshot("Entered 'San Francisco' into City Field");
154 | app.DismissKeyboard();
155 | app.ScrollDownTo(x => x.Marked("State").Sibling());
156 | app.Tap(x => x.Marked("State").Sibling());
157 | app.EnterText("CA");
158 | app.Screenshot("Entered 'CA' into State Field");
159 | app.DismissKeyboard();
160 | app.ScrollDownTo(x => x.Marked("ZIP").Sibling());
161 | app.Tap(x => x.Marked("ZIP").Sibling());
162 | app.EnterText("94111");
163 | app.Screenshot("Entered '94111' into City Field");
164 | app.DismissKeyboard();
165 | app.Tap(x => x.Class("ActionMenuItemView"));
166 | app.Screenshot("Saved contact");
167 | app.WaitForElement("NEW CONTACT", timeout:TimeSpan.FromSeconds(10));
168 | app.Screenshot("Verified on NEW CONTACT's Details Page");
169 | }
170 |
171 | [TestCase("message")]
172 | [TestCase("phone")]
173 | [TestCase("email")]
174 | [TestCase("directions")]
175 | public void VerifyExternalLink (string link)
176 | {
177 | Dictionary LinkIndexes = new Dictionary();
178 | LinkIndexes.Add("message", 2);
179 | LinkIndexes.Add("phone", 3);
180 | LinkIndexes.Add("email", 4);
181 | LinkIndexes.Add("directions", 1);
182 |
183 | app.Tap(x => x.Marked("Bell, Floyd"));
184 | app.Screenshot("Selected contact with name: 'Bell, Floyd'");
185 |
186 | app.Tap(x => x.Class("FormsImageView").Index(LinkIndexes[link]));
187 | System.Threading.Thread.Sleep(5000);
188 | app.Screenshot(String.Format("Verify {0} opened", link));
189 | }
190 | }
191 | }
192 |
193 |
--------------------------------------------------------------------------------
/Customers.UITests/Tests/iOSTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using NUnit.Framework;
5 | using Xamarin.UITest;
6 | using Xamarin.UITest.Queries;
7 |
8 | namespace Customers.UITests
9 | {
10 | [TestFixture(Platform.iOS)]
11 | public class iOSTests
12 | {
13 | IApp app;
14 | Platform platform;
15 |
16 | public iOSTests(Platform platform)
17 | {
18 | this.platform = platform;
19 | }
20 |
21 | [SetUp]
22 | public void BeforeEachTest()
23 | {
24 | app = AppInitializer.StartApp(platform);
25 | }
26 |
27 | [Test]
28 | public void EditCustomer ()
29 | {
30 | app.Tap(x => x.Marked("Armstead, Evan"));
31 | app.Screenshot("Selected contact with name: 'Armstead, Evan'");
32 | app.Tap(x => x.Marked("edit"));
33 | app.Screenshot("Tapped on 'edit' button");
34 | app.Tap(x => x.Marked("First").Sibling());
35 | app.ClearText(x => x.Marked("First").Sibling());
36 | app.EnterText("Edited");
37 | app.Screenshot("Entered 'Edited' into First Name Field");
38 | app.Tap(x => x.Marked("Last").Sibling());
39 | app.ClearText(x => x.Marked("Last").Sibling());
40 | app.EnterText("Name");
41 | app.Screenshot("Entered 'Name' into Last Name Field");
42 | app.Tap(x => x.Id("save"));
43 | app.Screenshot("Saved contact");
44 | app.WaitForElement(x => x.Text("Edited Name"));
45 | app.Screenshot("Verified contacts name changed to: 'Edited Name'");
46 | }
47 |
48 | [Test]
49 | public void EditCustomerInList ()
50 | {
51 | app.Tap(x => x.Marked("Cardell, Jesus"));
52 | app.Screenshot("Selected contact with name: 'Cardell, Jesus'");
53 | app.Tap(x => x.Marked("edit"));
54 | app.Screenshot("Tapped on 'edit' button");
55 | app.Tap(x => x.Marked("First").Sibling());
56 | app.ClearText(x => x.Marked("First").Sibling());
57 | app.EnterText("New Name");
58 | app.Screenshot("Entered 'New Name' into First Name Field");
59 | app.Tap(x => x.Marked("Last").Sibling());
60 | app.ClearText(x => x.Marked("Last").Sibling());
61 | app.EnterText("In List");
62 | app.Screenshot("Entered 'In List' into Last Name Field");
63 | app.Tap(x => x.Id("save"));
64 | app.Screenshot("Saved contact");
65 | app.Tap(x => x.Marked("Back"));
66 | app.Screenshot("Tapped on 'back' button");
67 | app.ScrollDownTo(x => x.Marked("In List, New Name"));
68 | app.Screenshot("Verified edited name appears in list");
69 | }
70 |
71 | [Test]
72 | public void UpdateCustomerAddress ()
73 | {
74 | app.Tap(x => x.Marked("Boone, John"));
75 | app.Screenshot("Selected contact with name: 'Boone, John'");
76 | app.WaitForElement(x => x.Class("MKNewAnnotationContainerView"));
77 | app.Screenshot("Verified map is set to current address");
78 | app.Tap(x => x.Marked("edit"));
79 | app.Screenshot("Tapped on 'edit' button");
80 | app.ScrollDownTo(x => x.Marked("Street").Sibling());
81 | app.Tap(x => x.Marked("Street").Sibling());
82 | app.ClearText(x => x.Marked("Street").Sibling());
83 | app.EnterText("394 Pacific Ave");
84 | app.Screenshot("Entered '394 Pacific Ave' into Street Field");
85 | app.PressEnter();
86 | app.ScrollDownTo("City");
87 | app.Tap(x => x.Marked("City").Sibling());
88 | app.ClearText(x => x.Marked("City").Sibling());
89 | app.EnterText("San Francisco");
90 | app.Screenshot("Entered 'San Francisco' into City Field");
91 | app.PressEnter();
92 | app.Tap(x => x.Marked("State").Sibling());
93 | app.ClearText(x => x.Marked("State").Sibling());
94 | app.EnterText("CA");
95 | app.Screenshot("Entered 'CA' into State Field");
96 | app.PressEnter();
97 | app.Tap(x => x.Marked("ZIP").Sibling());
98 | app.ClearText(x => x.Marked("ZIP").Sibling());
99 | app.EnterText("94111");
100 | app.Screenshot("Entered '94111' into ZIP Field");
101 | app.Tap(x => x.Id("save"));
102 | app.Screenshot("Saved contact");
103 | app.WaitForElement(x => x.Class("MKNewAnnotationContainerView"));
104 | app.Screenshot("Verified map is set to new address");
105 | }
106 |
107 | [Test]
108 | public void AddNewCustomer ()
109 | {
110 | app.Tap(x => x.Marked("add"));
111 | app.Screenshot("Tapped on 'add' button");
112 | app.Tap(x => x.Marked("First").Sibling());
113 | app.EnterText("NEW");
114 | app.Screenshot("Entered 'NEW' into First Name Field");
115 | app.PressEnter();
116 | app.Tap(x => x.Marked("Last").Sibling());
117 | app.EnterText("CONTACT");
118 | app.Screenshot("Entered 'CONTACT' into Last Name Field");
119 | app.PressEnter();
120 | app.Tap(x => x.Marked("Company").Sibling());
121 | app.EnterText("Xamarin");
122 | app.Screenshot("Entered 'Xamarin' into Company Field");
123 | app.PressEnter();
124 | app.Tap(x => x.Marked("Title").Sibling());
125 | app.EnterText("Test Cloud");
126 | app.Screenshot("Entered 'Test Cloud' into Title Field");
127 | app.PressEnter();
128 | app.Tap(x => x.Marked("Phone").Sibling());
129 | app.EnterText("1234567890");
130 | app.Screenshot("Entered '1234567890' into Phone Field");
131 | app.DismissKeyboard();
132 | app.Tap(x => x.Marked("Email").Sibling());
133 | app.EnterText("hello@xamarin.com");
134 | app.Screenshot("Entered 'hello@xamarin.com' into Email Field");
135 | app.PressEnter();
136 | app.Tap(x => x.Marked("Street").Sibling());
137 | app.EnterText("394 Pacific Ave");
138 | app.Screenshot("Entered '394 Pacific Ave' into Street Field");
139 | app.PressEnter();
140 | app.Tap(x => x.Marked("City").Sibling());
141 | app.EnterText("San Francisco");
142 | app.Screenshot("Entered 'San Francisco' into City Field");
143 | app.PressEnter();
144 | app.Tap(x => x.Marked("State").Sibling());
145 | app.EnterText("CA");
146 | app.Screenshot("Entered 'CA' into State Field");
147 | app.PressEnter();
148 | app.Tap(x => x.Marked("ZIP").Sibling());
149 | app.EnterText("94111");
150 | app.Screenshot("Entered '94111' into City Field");
151 | app.DismissKeyboard();
152 | app.Tap(x => x.Id("save"));
153 | app.Screenshot("Saved contact");
154 | app.WaitForElement("NEW CONTACT", timeout:TimeSpan.FromSeconds(10));
155 | app.Screenshot("Verified on NEW CONTACT's Details Page");
156 | }
157 |
158 | [TestCase("message")]
159 | [TestCase("phone")]
160 | [TestCase("email")]
161 | [TestCase("directions")]
162 | public void VerifyExternalLink (string link)
163 | {
164 | app.Tap(x => x.Marked("Bell, Floyd"));
165 | app.Screenshot("Selected contact with name: 'Bell, Floyd'");
166 |
167 | app.Tap(link);
168 | System.Threading.Thread.Sleep(5000);
169 | app.Screenshot(String.Format("Verify {0} opened", link));
170 | }
171 | }
172 | }
173 |
174 |
--------------------------------------------------------------------------------
/Customers.UITests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Customers.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2012
4 | VisualStudioVersion = 14.0.24720.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers", "Customers\Customers.csproj", "{55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers.iOS", "iOS\Customers.iOS.csproj", "{38CAF316-C475-4099-9723-A4002F76FCD2}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers.Droid", "Droid\Customers.Droid.csproj", "{186E2C95-63F1-44B9-AE99-0F436E3DADFE}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Customers.UITests", "Customers.UITests\Customers.UITests.csproj", "{02FC1245-00EF-4946-9C94-1CCA2CF51B66}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Debug|iPhone = Debug|iPhone
18 | Debug|iPhoneSimulator = Debug|iPhoneSimulator
19 | Debug|x86 = Debug|x86
20 | Release|Any CPU = Release|Any CPU
21 | Release|iPhone = Release|iPhone
22 | Release|iPhoneSimulator = Release|iPhoneSimulator
23 | Release|x86 = Release|x86
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhone.ActiveCfg = Debug|Any CPU
29 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhone.Build.0 = Debug|Any CPU
30 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
31 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
32 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|x86.ActiveCfg = Debug|Any CPU
33 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Debug|x86.Build.0 = Debug|Any CPU
34 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhone.ActiveCfg = Release|Any CPU
37 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhone.Build.0 = Release|Any CPU
38 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
39 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
40 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|x86.ActiveCfg = Release|Any CPU
41 | {02FC1245-00EF-4946-9C94-1CCA2CF51B66}.Release|x86.Build.0 = Release|Any CPU
42 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
43 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
44 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
45 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhone.ActiveCfg = Debug|Any CPU
46 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhone.Build.0 = Debug|Any CPU
47 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
48 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
49 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|x86.ActiveCfg = Debug|Any CPU
50 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Debug|x86.Build.0 = Debug|Any CPU
51 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
52 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|Any CPU.Build.0 = Release|Any CPU
53 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhone.ActiveCfg = Release|Any CPU
54 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhone.Build.0 = Release|Any CPU
55 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
56 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
57 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|x86.ActiveCfg = Release|Any CPU
58 | {186E2C95-63F1-44B9-AE99-0F436E3DADFE}.Release|x86.Build.0 = Release|Any CPU
59 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator
60 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator
61 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhone.ActiveCfg = Debug|iPhone
62 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhone.Build.0 = Debug|iPhone
63 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
64 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
65 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|x86.ActiveCfg = Debug|iPhoneSimulator
66 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Debug|x86.Build.0 = Debug|iPhoneSimulator
67 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|Any CPU.ActiveCfg = Release|iPhone
68 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|Any CPU.Build.0 = Release|iPhone
69 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhone.ActiveCfg = Release|iPhone
70 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhone.Build.0 = Release|iPhone
71 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
72 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
73 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|x86.ActiveCfg = Release|iPhone
74 | {38CAF316-C475-4099-9723-A4002F76FCD2}.Release|x86.Build.0 = Release|iPhone
75 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
76 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|Any CPU.Build.0 = Debug|Any CPU
77 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhone.ActiveCfg = Debug|Any CPU
78 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhone.Build.0 = Debug|Any CPU
79 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
80 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
81 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|x86.ActiveCfg = Debug|Any CPU
82 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Debug|x86.Build.0 = Debug|Any CPU
83 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|Any CPU.ActiveCfg = Release|Any CPU
84 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|Any CPU.Build.0 = Release|Any CPU
85 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhone.ActiveCfg = Release|Any CPU
86 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhone.Build.0 = Release|Any CPU
87 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
88 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
89 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|x86.ActiveCfg = Release|Any CPU
90 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}.Release|x86.Build.0 = Release|Any CPU
91 | EndGlobalSection
92 | GlobalSection(SolutionProperties) = preSolution
93 | HideSolutionNode = FALSE
94 | EndGlobalSection
95 | EndGlobal
96 |
--------------------------------------------------------------------------------
/Customers/App.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Customers/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using Xamarin.Forms;
2 | using FormsToolkit;
3 |
4 | namespace Customers
5 | {
6 | public partial class App : Application
7 | {
8 | public App()
9 | {
10 | InitializeComponent();
11 |
12 | SubscribeToDisplayAlertMessages();
13 |
14 | if (Device.OS == TargetPlatform.iOS)
15 | navPage.BarTextColor = Color.White;
16 | }
17 |
18 | ///
19 | /// Subscribes to messages for displaying alerts.
20 | ///
21 | static void SubscribeToDisplayAlertMessages()
22 | {
23 | MessagingService.Current.Subscribe(MessageKeys.DisplayAlert, async (service, info) =>
24 | {
25 | var task = Application.Current?.MainPage?.DisplayAlert(info.Title, info.Message, info.Cancel);
26 | if (task != null)
27 | {
28 | await task;
29 | info?.OnCompleted?.Invoke();
30 | }
31 | });
32 |
33 | MessagingService.Current.Subscribe(MessageKeys.DisplayQuestion, async (service, info) =>
34 | {
35 | var task = Application.Current?.MainPage?.DisplayAlert(info.Title, info.Question, info.Positive, info.Negative);
36 | if (task != null)
37 | {
38 | var result = await task;
39 | info?.OnCompleted?.Invoke(result);
40 | }
41 | });
42 |
43 | MessagingService.Current.Subscribe(MessageKeys.DisplayGeocodingError, async (service) =>
44 | {
45 | var task = Application.Current?.MainPage?.DisplayAlert("Geocoding Error", "An eror occurred while converting the street address to GPS coordinates.", "OK");
46 | if (task != null)
47 | await task;
48 | });
49 | }
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/Customers/Constants/MessageKeys.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Customers
4 | {
5 | public static class MessageKeys
6 | {
7 | public const string DisplayAlert = "DisplayAlert";
8 | public const string DisplayQuestion = "DisplayQuestion";
9 | public const string SaveCustomer = "SaveCustomer";
10 | public const string DeleteCustomer = "DeleteCustomer";
11 | public const string CustomerLocationUpdated = "CustomerLocationUpdated";
12 | public const string SetupMap = "SetupMap";
13 | public const string DisplayGeocodingError = "DisplayGeocodingError";
14 | }
15 | }
16 |
17 |
--------------------------------------------------------------------------------
/Customers/Converters/BooleanInverter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xamarin.Forms;
3 | using System.Globalization;
4 |
5 | namespace Customers
6 | {
7 | public class BooleanInverter : IValueConverter
8 | {
9 | #region IValueConverter implementation
10 | public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
11 | {
12 | return !(bool)value;
13 | }
14 | public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
15 | {
16 | return !(bool)value;
17 | }
18 | #endregion
19 |
20 | }
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/Customers/Customers.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
7 | {55CEDCD6-E2D6-48EB-B9C6-A0E74016DCE0}
8 | Library
9 | Customers
10 | Customers
11 | v4.5
12 | Profile78
13 |
14 |
15 | true
16 | full
17 | false
18 | bin\Debug
19 | DEBUG;
20 | prompt
21 | 4
22 | false
23 |
24 |
25 | full
26 | true
27 | bin\Release
28 | prompt
29 | 4
30 | false
31 |
32 |
33 |
34 |
35 | App.xaml
36 |
37 |
38 | CustomerListPage.xaml
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | CustomerEditPage.xaml
49 |
50 |
51 | CustomerDetailPage.xaml
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | ..\packages\Xamarin.Forms.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Core.dll
69 |
70 |
71 | ..\packages\Xamarin.Forms.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Xaml.dll
72 |
73 |
74 | ..\packages\Xamarin.Forms.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Platform.dll
75 |
76 |
77 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.dll
78 |
79 |
80 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Primitives.dll
81 |
82 |
83 | ..\packages\Microsoft.Net.Http.2.2.29\lib\portable-net40+sl4+win8+wp71+wpa81\System.Net.Http.Extensions.dll
84 |
85 |
86 | ..\packages\Microsoft.Azure.Mobile.Client.2.0.1\lib\portable-win+net45+wp8+wpa81+monotouch+monoandroid\Microsoft.WindowsAzure.Mobile.dll
87 |
88 |
89 | ..\packages\SQLitePCL.3.8.7.2\lib\portable-net45+sl50+win+wpa81+wp80+MonoAndroid10+xamarinios10+MonoTouch10\SQLitePCL.dll
90 |
91 |
92 | ..\packages\Microsoft.Azure.Mobile.Client.SQLiteStore.2.0.1\lib\portable-win+net45+wp8+wpa81+monotouch+monoandroid\Microsoft.WindowsAzure.Mobile.SQLiteStore.dll
93 |
94 |
95 | ..\packages\PCLStorage.1.0.2\lib\portable-net45+wp8+wpa81+win8+monoandroid+monotouch+Xamarin.iOS+Xamarin.Mac\PCLStorage.dll
96 |
97 |
98 | ..\packages\PCLStorage.1.0.2\lib\portable-net45+wp8+wpa81+win8+monoandroid+monotouch+Xamarin.iOS+Xamarin.Mac\PCLStorage.Abstractions.dll
99 |
100 |
101 | ..\packages\Xam.Plugins.Forms.ImageCircle.1.3.0\lib\portable-net45+wp8+win8+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\ImageCircle.Forms.Plugin.Abstractions.dll
102 |
103 |
104 | ..\packages\Xamarin.Forms.Maps.2.0.0.6490\lib\portable-win+net45+wp80+win81+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10\Xamarin.Forms.Maps.dll
105 |
106 |
107 | ..\packages\Xam.Plugins.Messaging.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Lotz.Xam.Messaging.Abstractions.dll
108 |
109 |
110 | ..\packages\Xam.Plugins.Messaging.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Lotz.Xam.Messaging.dll
111 |
112 |
113 | ..\packages\Newtonsoft.Json.8.0.2\lib\portable-net45+wp80+win8+wpa81+dnxcore50\Newtonsoft.Json.dll
114 |
115 |
116 | ..\packages\Refractored.MvvmHelpers.1.0.1\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\MvvmHelpers.dll
117 |
118 |
119 | ..\packages\Xamarin.Insights.1.11.3\lib\portable-win+net45+wp80+windows8+wpa+MonoAndroid10+MonoTouch10\Xamarin.Insights.dll
120 |
121 |
122 | ..\packages\Xam.Plugin.ExternalMaps.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Plugin.ExternalMaps.dll
123 |
124 |
125 | ..\packages\Xam.Plugin.ExternalMaps.3.0.0\lib\portable-net45+wp8+wpa81+win8+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\Plugin.ExternalMaps.Abstractions.dll
126 |
127 |
128 | ..\packages\FormsToolkit.1.1.11\lib\portable-net45+wp8+win8+wpa81+MonoAndroid10+MonoTouch10+Xamarin.iOS10+UAP10\FormsToolkit.dll
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | MSBuild:UpdateDesignTimeXaml
137 |
138 |
139 | MSBuild:UpdateDesignTimeXaml
140 |
141 |
142 | MSBuild:UpdateDesignTimeXaml
143 |
144 |
145 | MSBuild:UpdateDesignTimeXaml
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
--------------------------------------------------------------------------------
/Customers/Data/Customer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Newtonsoft.Json;
3 | using MvvmHelpers;
4 |
5 | namespace Customers
6 | {
7 | public class Customer : ObservableObject
8 | {
9 | public Customer()
10 | {
11 | Id = Guid.NewGuid().ToString();
12 | PhotoUrl = "placeholderProfileImage";
13 | }
14 |
15 | public string Id { get; set; }
16 |
17 | string _FirstName;
18 | public string FirstName
19 | {
20 | get { return _FirstName; }
21 | set
22 | {
23 | SetProperty(ref _FirstName, value);
24 | OnPropertyChanged(nameof(DisplayName)); // because DisplayName is dependent on FirstName, we need to manually call OnPropertyChanged() on DisplayName
25 | OnPropertyChanged(nameof(DisplayLastNameFirst)); // because DisplayLastNameFirst is dependent on FirstName, we need to manually call OnPropertyChanged() on DisplayLastNameFirst
26 | }
27 | }
28 |
29 | string _LastName;
30 | public string LastName
31 | {
32 | get { return _LastName; }
33 | set
34 | {
35 | SetProperty(ref _LastName, value);
36 | OnPropertyChanged(nameof(DisplayName)); // because DisplayName is dependent on LastName, we need to manually call OnPropertyChanged() on DisplayName
37 | OnPropertyChanged(nameof(DisplayLastNameFirst)); // because DisplayLastNameFirst is dependent on LastName, we need to manually call OnPropertyChanged() on DisplayLastNameFirst
38 | }
39 | }
40 |
41 | string _Company;
42 | public string Company
43 | {
44 | get { return _Company; }
45 | set { SetProperty(ref _Company, value); }
46 | }
47 |
48 | string _JobTitle;
49 | public string JobTitle
50 | {
51 | get { return _JobTitle; }
52 | set { SetProperty(ref _JobTitle, value); }
53 | }
54 |
55 | string _Email;
56 | public string Email
57 | {
58 | get { return _Email; }
59 | set { SetProperty(ref _Email, value); }
60 | }
61 |
62 | string _Phone;
63 | public string Phone
64 | {
65 | get { return _Phone; }
66 | set { SetProperty(ref _Phone, value); }
67 | }
68 |
69 | string _Street;
70 | public string Street
71 | {
72 | get { return _Street; }
73 | set
74 | {
75 | SetProperty(ref _Street, value);
76 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on Street, we need to manually call OnPropertyChanged() on AddressString
77 | }
78 | }
79 |
80 | string _City;
81 | public string City
82 | {
83 | get { return _City; }
84 | set
85 | {
86 | SetProperty(ref _City, value);
87 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on City, we need to manually call OnPropertyChanged() on AddressString
88 | }
89 | }
90 |
91 | string _PostalCode;
92 | public string PostalCode
93 | {
94 | get { return _PostalCode; }
95 | set
96 | {
97 | SetProperty(ref _PostalCode, value);
98 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on PostalCode, we need to manually call OnPropertyChanged() on AddressString
99 | OnPropertyChanged(nameof(StatePostal)); // because StatePostal is dependent on PostalCode, we need to manually call OnPropertyChanged() on StatePostal
100 | }
101 | }
102 |
103 |
104 | string _State;
105 | public string State
106 | {
107 | get { return _State; }
108 | set
109 | {
110 | SetProperty(ref _State, value);
111 | OnPropertyChanged(nameof(AddressString)); // because AddressString is dependent on State, we need to manually call OnPropertyChanged() on AddressString
112 | OnPropertyChanged(nameof(StatePostal)); // because StatePostal is dependent on State, we need to manually call OnPropertyChanged() on StatePostal
113 | }
114 | }
115 |
116 | string _PhotoUrl;
117 | public string PhotoUrl
118 | {
119 | get { return _PhotoUrl; }
120 | set
121 | {
122 | SetProperty(ref _PhotoUrl, value);
123 | OnPropertyChanged(nameof(SmallPhotoUrl)); // because SmallPhotoUrl is dependent on PhotoUrl, we need to manually call OnPropertyChanged() on SmallPhotoUrl
124 | }
125 | }
126 |
127 | public string SmallPhotoUrl { get { return PhotoUrl; }}
128 |
129 | [JsonIgnore]
130 | public string AddressString
131 | {
132 | get
133 | {
134 | return string.Format(
135 | "{0} {1} {2} {3}",
136 | Street,
137 | !string.IsNullOrWhiteSpace(City) ? City + "," : "",
138 | State,
139 | PostalCode);
140 | }
141 | }
142 |
143 | [JsonIgnore]
144 | public string DisplayName
145 | {
146 | get { return this.ToString(); }
147 | }
148 |
149 | [JsonIgnore]
150 | public string DisplayLastNameFirst
151 | {
152 | get { return String.Format("{0}, {1}", LastName, FirstName); }
153 | }
154 |
155 | [JsonIgnore]
156 | public string StatePostal
157 | {
158 | get { return State + " " + PostalCode; }
159 | }
160 |
161 | public override string ToString()
162 | {
163 | return FirstName + " " + LastName;
164 | }
165 | }
166 | }
167 |
168 |
--------------------------------------------------------------------------------
/Customers/Data/IDataSource.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using System.Collections.Generic;
3 |
4 | namespace Customers
5 | {
6 | public interface IDataSource
7 | {
8 | Task SaveItem(T item);
9 | Task DeleteItem(string id);
10 | Task GetItem(string id);
11 | Task> GetItems(int start = 0, int count = 100, string query = "");
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/Customers/Data/ILocalization.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 |
3 | namespace Customers
4 | {
5 | public interface ILocalization
6 | {
7 | CultureInfo GetCurrentCultureInfo ();
8 |
9 | string ToTitleCase(string value);
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/Customers/Extensions/IntExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Customers
4 | {
5 | public static class IntExtensions
6 | {
7 | public static int RoundToLowestHundreds (this int i)
8 | {
9 | return ((int)Math.Floor(i / 100.0)) * 100;
10 | }
11 | }
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/Customers/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | using Xamarin.Forms;
4 | using System.Linq;
5 |
6 | namespace Customers
7 | {
8 | public static class StringExtensions
9 | {
10 | public static string SanitizePhoneNumber(this string value)
11 | {
12 | return new String(value.ToCharArray().Where(Char.IsDigit).ToArray());
13 | }
14 |
15 | public static string ToTitleCase(this string input)
16 | {
17 | return DependencyService.Get().ToTitleCase(input);
18 | }
19 |
20 | public static bool IsNullOrWhiteSpace(this string value)
21 | {
22 | return string.IsNullOrWhiteSpace(value);
23 | }
24 | }
25 | }
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Customers/Pages/CustomerDetailPage.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
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 |
--------------------------------------------------------------------------------
/Customers/Pages/CustomerDetailPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using Xamarin.Forms;
2 | using Xamarin.Forms.Maps;
3 | using System.Threading.Tasks;
4 | using FormsToolkit;
5 | using Xamarin;
6 | using System;
7 |
8 | namespace Customers
9 | {
10 | public partial class CustomerDetailPage : ContentPage
11 | {
12 | protected CustomerDetailViewModel ViewModel
13 | {
14 | get { return BindingContext as CustomerDetailViewModel; }
15 | }
16 |
17 | public CustomerDetailPage()
18 | {
19 | InitializeComponent();
20 | }
21 |
22 | async protected override void OnAppearing()
23 | {
24 | base.OnAppearing();
25 |
26 | // Typically, is preferable to call into the viewmodel for OnAppearing() logic to be performed,
27 | // but we're not doing that in this case because we need to interact with the Xamarin.Forms.Map property on this Page.
28 | // In the future, the Map type and it's properties may get more binding support, so that the map setup can be omitted from code-behind.
29 | await SetupMap();
30 | }
31 |
32 | ///
33 | /// Sets up the map.
34 | ///
35 | /// A Task.
36 | async Task SetupMap()
37 | {
38 | if (ViewModel.HasAddress)
39 | {
40 |
41 | ViewModel.IsBusy = true;
42 | Map.IsVisible = false;
43 |
44 | // set to a default position
45 | Position position;
46 |
47 | try
48 | {
49 | position = await ViewModel.GetPosition();
50 | }
51 | catch (Exception ex)
52 | {
53 | MessagingService.Current.SendMessage(MessageKeys.DisplayGeocodingError);
54 |
55 | ViewModel.IsBusy = false;
56 |
57 | // TODO: Show insights
58 | Insights.Report(ex, Insights.Severity.Error);
59 |
60 | return;
61 | }
62 |
63 | // if lat and lon are both 0, then it's assumed that position acquisition failed
64 | if (position.Latitude == 0 && position.Longitude == 0)
65 | {
66 | MessagingService.Current.SendMessage(MessageKeys.DisplayGeocodingError);
67 |
68 | ViewModel.IsBusy = false;
69 |
70 | return;
71 | }
72 | else
73 | {
74 | var pin = new Pin()
75 | {
76 | Type = PinType.Place,
77 | Position = position,
78 | Label = ViewModel.Customer.DisplayName,
79 | Address = ViewModel.Customer.AddressString
80 | };
81 |
82 | Map.Pins.Clear();
83 |
84 | Map.Pins.Add(pin);
85 |
86 | Map.MoveToRegion(MapSpan.FromCenterAndRadius(pin.Position, Distance.FromMiles(10)));
87 |
88 | Map.IsVisible = true;
89 | ViewModel.IsBusy = false;
90 | }
91 | }
92 | }
93 | }
94 | }
95 |
96 |
--------------------------------------------------------------------------------
/Customers/Pages/CustomerEditPage.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Customers/Pages/CustomerEditPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using Xamarin.Forms;
3 |
4 | namespace Customers
5 | {
6 | public partial class CustomerEditPage : ContentPage
7 | {
8 | protected CustomerDetailViewModel ViewModel
9 | {
10 | get { return BindingContext as CustomerDetailViewModel; }
11 | }
12 |
13 | public CustomerEditPage()
14 | {
15 | InitializeComponent();
16 |
17 | if (Device.OS == TargetPlatform.iOS)
18 | Title = null; // because iOS already displays the previous page's title with the back button, we don't want to display it twice.
19 | }
20 |
21 | ///
22 | /// Ensures the state field has 2 characters at most.
23 | ///
24 | /// The sender.
25 | /// The PropertyChangedEventArgs
26 | void StateEntry_PropertyChanged (object sender, PropertyChangedEventArgs e)
27 | {
28 | if (e.PropertyName == "Text")
29 | {
30 | var entryCell = sender as EntryCell;
31 |
32 | string val = entryCell.Text;
33 |
34 | if (val.Length > 2)
35 | {
36 | val = val.Remove(val.Length - 1);
37 | }
38 |
39 | entryCell.Text = val.ToUpperInvariant();
40 | }
41 | }
42 |
43 | ///
44 | /// Ensures the zip code field has 5 characters at most.
45 | ///
46 | /// The sender.
47 | /// The PropertyChangedEventArgs
48 | void PostalCode_PropertyChanged (object sender, PropertyChangedEventArgs e)
49 | {
50 | if (e.PropertyName == "Text")
51 | {
52 | var entryCell = sender as EntryCell;
53 |
54 | string val = entryCell.Text;
55 |
56 | if (val.Length > 5)
57 | {
58 | val = val.Remove(val.Length - 1);
59 | entryCell.Text = val;
60 | }
61 | }
62 |
63 | }
64 | }
65 | }
66 |
67 |
--------------------------------------------------------------------------------
/Customers/Pages/CustomerListPage.xaml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Customers/Pages/CustomerListPage.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using FormsToolkit;
3 | using Xamarin.Forms;
4 |
5 | namespace Customers
6 | {
7 | public partial class CustomerListPage : ContentPage
8 | {
9 | protected CustomerListViewModel ViewModel
10 | {
11 | get { return BindingContext as CustomerListViewModel; }
12 | }
13 |
14 | public CustomerListPage()
15 | {
16 | InitializeComponent();
17 |
18 | // on Android, we use a floating action button
19 | if (Device.OS == TargetPlatform.Android)
20 | ToolbarItems.Clear();
21 |
22 | fab.Clicked = AndroidAddButtonClicked;
23 | }
24 |
25 | ///
26 | /// The action to take when a list item is tapped.
27 | ///
28 | /// The sender.
29 | /// The ItemTappedEventArgs
30 | void ItemTapped (object sender, ItemTappedEventArgs e)
31 | {
32 | Navigation.PushAsync(new CustomerDetailPage() { BindingContext = new CustomerDetailViewModel((Customer)e.Item) });
33 |
34 | ((ListView)sender).SelectedItem = null;
35 | }
36 |
37 | ///
38 | /// The action to take when the + button is clicked on Android.
39 | ///
40 | /// The sender.
41 | /// The EventArgs
42 | void AndroidAddButtonClicked (object sender, EventArgs e)
43 | {
44 | Navigation.PushAsync(new CustomerEditPage() { BindingContext = new CustomerDetailViewModel(new Customer()) });
45 | }
46 |
47 | protected override void OnAppearing()
48 | {
49 | base.OnAppearing();
50 |
51 | ViewModel.LoadCustomersCommand.Execute(null);
52 | }
53 | }
54 | }
55 |
56 |
--------------------------------------------------------------------------------
/Customers/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 |
4 | // Information about this assembly is defined by the following attributes.
5 | // Change them to the values specific to your project.
6 |
7 | [assembly: AssemblyTitle("Customers")]
8 | [assembly: AssemblyDescription("")]
9 | [assembly: AssemblyConfiguration("")]
10 | [assembly: AssemblyCompany("Xamarin Inc.")]
11 | [assembly: AssemblyProduct("")]
12 | [assembly: AssemblyCopyright("Xamarin Inc.")]
13 | [assembly: AssemblyTrademark("")]
14 | [assembly: AssemblyCulture("")]
15 |
16 | // The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
17 | // The form "{Major}.{Minor}.*" will automatically update the build and revision,
18 | // and "{Major}.{Minor}.{Build}.*" will update just the revision.
19 |
20 | [assembly: AssemblyVersion("1.0.*")]
21 |
22 | // The following attributes are used to specify the signing key for the assembly,
23 | // if desired. See the Mono documentation for more information about signing.
24 |
25 | //[assembly: AssemblyDelaySign(false)]
26 | //[assembly: AssemblyKeyFile("")]
27 |
28 |
--------------------------------------------------------------------------------
/Customers/Services/CapabilityService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xamarin.Forms;
3 | using Customers;
4 |
5 | [assembly: Xamarin.Forms.Dependency (typeof (CapabilityService))]
6 |
7 | namespace Customers
8 | {
9 | public class CapabilityService : ICapabilityService
10 | {
11 | readonly IEnvironmentService _EnvironmentService;
12 |
13 | public CapabilityService()
14 | {
15 | _EnvironmentService = DependencyService.Get();
16 | }
17 |
18 | #region ICapabilityService implementation
19 |
20 | public bool CanMakeCalls
21 | {
22 | get
23 | {
24 | return _EnvironmentService.IsRealDevice || (Device.OS != TargetPlatform.iOS);
25 | }
26 | }
27 |
28 | public bool CanSendMessages
29 | {
30 | get
31 | {
32 | return _EnvironmentService.IsRealDevice || (Device.OS != TargetPlatform.iOS);
33 | }
34 | }
35 |
36 | public bool CanSendEmail
37 | {
38 | get
39 | {
40 | return _EnvironmentService.IsRealDevice || (Device.OS != TargetPlatform.iOS);
41 | }
42 | }
43 |
44 | #endregion
45 | }
46 | }
47 |
48 |
--------------------------------------------------------------------------------
/Customers/Services/ICapabilityService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Customers
4 | {
5 | public interface ICapabilityService
6 | {
7 | bool CanMakeCalls { get; }
8 | bool CanSendMessages { get; }
9 | bool CanSendEmail { get; }
10 | }
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/Customers/Services/IEnvironmentService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace Customers
4 | {
5 | public interface IEnvironmentService
6 | {
7 | bool IsRealDevice { get; }
8 | }
9 | }
10 |
11 |
--------------------------------------------------------------------------------
/Customers/ViewModels/BaseNavigationViewModel.cs:
--------------------------------------------------------------------------------
1 | using MvvmHelpers;
2 | using Xamarin.Forms;
3 | using System.Threading.Tasks;
4 | using System.Collections.Generic;
5 |
6 | namespace Customers
7 | {
8 | ///
9 | /// Implements the INavigation interface on top of BaseViewModel.
10 | ///
11 | public abstract class BaseNavigationViewModel : BaseViewModel, INavigation
12 | {
13 | readonly INavigation _Navigation;
14 |
15 | protected BaseNavigationViewModel()
16 | {
17 | // If Navigation is available on Application.Current.MainPage, get it.
18 | _Navigation = Application.Current?.MainPage?.Navigation;
19 | }
20 |
21 | #region INavigation implementation
22 |
23 | public void RemovePage(Page page)
24 | {
25 | _Navigation?.RemovePage(page);
26 | }
27 |
28 | public void InsertPageBefore(Page page, Page before)
29 | {
30 | _Navigation?.InsertPageBefore(page, before);
31 | }
32 |
33 | public async Task PushAsync(Page page)
34 | {
35 | var task = _Navigation?.PushAsync(page);
36 | if (task != null)
37 | await task;
38 | }
39 |
40 | public async Task PopAsync()
41 | {
42 | var task = _Navigation?.PopAsync();
43 | return task != null ? await task : await Task.FromResult(null as Page);
44 | }
45 |
46 | public async Task PopToRootAsync()
47 | {
48 | var task = _Navigation?.PopToRootAsync();
49 | if (task != null)
50 | await task;
51 | }
52 |
53 | public async Task PushModalAsync(Page page)
54 | {
55 | var task = _Navigation?.PushModalAsync(page);
56 | if (task != null)
57 | await task;
58 | }
59 |
60 | public async Task PopModalAsync()
61 | {
62 | var task = _Navigation?.PopModalAsync();
63 | return (task != null) ? await task : await Task.FromResult(null as Page);
64 | }
65 |
66 | public async Task PushAsync(Page page, bool animated)
67 | {
68 | var task = _Navigation?.PushAsync(page, animated);
69 | if (task != null)
70 | await task;
71 | }
72 |
73 | public async Task PopAsync(bool animated)
74 | {
75 | var task = _Navigation?.PopAsync(animated);
76 | return (task != null) ? await task : await Task.FromResult(null as Page);
77 | }
78 |
79 | public async Task PopToRootAsync(bool animated)
80 | {
81 | var task = _Navigation?.PopToRootAsync(animated);
82 | if (task != null)
83 | await task;
84 | }
85 |
86 | public async Task PushModalAsync(Page page, bool animated)
87 | {
88 | var task = _Navigation?.PushModalAsync(page, animated);
89 | if (task != null)
90 | await task;
91 | }
92 |
93 | public async Task PopModalAsync(bool animated)
94 | {
95 | var task = _Navigation?.PopModalAsync(animated);
96 | return (task != null) ? await task : await Task.FromResult(null as Page);
97 | }
98 |
99 | public IReadOnlyList NavigationStack
100 | {
101 | get { return _Navigation?.NavigationStack; }
102 | }
103 |
104 | public IReadOnlyList ModalStack
105 | {
106 | get { return _Navigation?.ModalStack; }
107 | }
108 |
109 | #endregion
110 | }
111 | }
112 |
113 |
--------------------------------------------------------------------------------
/Customers/ViewModels/CustomerDetailViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using FormsToolkit;
5 | using Plugin.ExternalMaps;
6 | using Plugin.ExternalMaps.Abstractions;
7 | using Plugin.Messaging;
8 | using Xamarin.Forms;
9 | using Xamarin.Forms.Maps;
10 |
11 | namespace Customers
12 | {
13 | public class CustomerDetailViewModel : BaseNavigationViewModel
14 | {
15 | bool _IsNewCustomer;
16 |
17 | // this is just a utility service that we're using in this demo app to mitigate some limitations of the iOS simulator
18 | ICapabilityService _CapabilityService;
19 |
20 | readonly Geocoder _Geocoder;
21 |
22 | public CustomerDetailViewModel(Customer customer = null)
23 | {
24 | _CapabilityService = DependencyService.Get();
25 |
26 | _Geocoder = new Geocoder();
27 |
28 | if (customer == null)
29 | {
30 | _IsNewCustomer = true;
31 | Customer = new Customer();
32 | }
33 | else
34 | {
35 | _IsNewCustomer = false;
36 | Customer = customer;
37 | }
38 |
39 | Title = _IsNewCustomer ? "New Customer" : _Customer.DisplayLastNameFirst;
40 |
41 | _AddressString = Customer.AddressString;
42 |
43 | SubscribeToSaveCustomerMessages();
44 |
45 | SubscribeToCustomerLocationUpdatedMessages();
46 | }
47 |
48 | public bool IsExistingCustomer { get { return !_IsNewCustomer; } }
49 |
50 | public bool HasEmailAddress { get { return Customer != null && !String.IsNullOrWhiteSpace(Customer.Email); } }
51 |
52 | public bool HasPhoneNumber { get { return Customer != null && !String.IsNullOrWhiteSpace(Customer.Phone); } }
53 |
54 | public bool HasAddress { get { return Customer != null && !String.IsNullOrWhiteSpace(Customer.AddressString); } }
55 |
56 | private string _AddressString;
57 |
58 | Customer _Customer;
59 |
60 | public Customer Customer
61 | {
62 | get { return _Customer; }
63 | set
64 | {
65 | _Customer = value;
66 | OnPropertyChanged("Customer");
67 | }
68 | }
69 |
70 | Command _SaveCustomerCommand;
71 |
72 | ///
73 | /// Command to save customer
74 | ///
75 | public Command SaveCustomerCommand
76 | {
77 | get
78 | {
79 | return _SaveCustomerCommand ??
80 | (_SaveCustomerCommand = new Command(() => ExecuteSaveCustomerCommand()));
81 | }
82 | }
83 |
84 | async Task ExecuteSaveCustomerCommand()
85 | {
86 | if (String.IsNullOrWhiteSpace(Customer.LastName) || String.IsNullOrWhiteSpace(Customer.FirstName))
87 | {
88 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
89 | {
90 | Title = "Invalid name!",
91 | Message = "A customer must have both a first and last name.",
92 | Cancel = "OK"
93 | });
94 | }
95 | else if (!RequiredAddressFieldCombinationIsFilled)
96 | {
97 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
98 | {
99 | Title = "Invalid address!",
100 | Message = "You must enter either a street, city, and state combination, or a postal code.",
101 | Cancel = "OK"
102 | });
103 | }
104 | else
105 | {
106 | MessagingService.Current.SendMessage(MessageKeys.SaveCustomer, this.Customer);
107 |
108 | await PopAsync();
109 | }
110 | }
111 |
112 | bool RequiredAddressFieldCombinationIsFilled
113 | {
114 | get
115 | {
116 | if (Customer.AddressString.IsNullOrWhiteSpace())
117 | {
118 | return true;
119 | }
120 | else if (!Customer.Street.IsNullOrWhiteSpace() && (!Customer.City.IsNullOrWhiteSpace() && !Customer.State.IsNullOrWhiteSpace()))
121 | {
122 | return true;
123 | }
124 | else if (!Customer.PostalCode.IsNullOrWhiteSpace() && (Customer.Street.IsNullOrWhiteSpace() || Customer.City.IsNullOrWhiteSpace() || Customer.State.IsNullOrWhiteSpace()))
125 | {
126 | return true;
127 | }
128 |
129 | return false;
130 | }
131 | }
132 |
133 | Command _EditCustomerCommand;
134 |
135 | ///
136 | /// Command to edit customer
137 | ///
138 | public Command EditCustomerCommand
139 | {
140 | get
141 | {
142 | return _EditCustomerCommand ??
143 | (_EditCustomerCommand = new Command(async () => await ExecuteEditCustomerCommand()));
144 | }
145 | }
146 |
147 | async Task ExecuteEditCustomerCommand()
148 | {
149 | await PushAsync(new CustomerEditPage() { BindingContext = this });
150 | }
151 |
152 |
153 | Command _DeleteCustomerCommand;
154 |
155 | ///
156 | /// Command to delete customer
157 | ///
158 | public Command DeleteCustomerCommand
159 | {
160 | get
161 | {
162 | return _DeleteCustomerCommand ??
163 | (_DeleteCustomerCommand = new Command(ExecuteDeleteCustomerCommand));
164 | }
165 | }
166 |
167 | void ExecuteDeleteCustomerCommand()
168 | {
169 | MessagingService.Current.SendMessage(MessageKeys.DisplayQuestion, new MessagingServiceQuestion()
170 | {
171 | Title = String.Format("Delete {0}?", Customer.DisplayName),
172 | Question = null,
173 | Positive = "Delete",
174 | Negative = "Cancel",
175 | OnCompleted = new Action(async result =>
176 | {
177 | if (result)
178 | {
179 | await PopAsync(false);
180 |
181 | await PopAsync();
182 |
183 | // send a message that we want the given customer to be deleted
184 | MessagingService.Current.SendMessage(MessageKeys.DeleteCustomer, this.Customer);
185 | }
186 | })
187 | });
188 | }
189 |
190 | Command _DialNumberCommand;
191 |
192 | ///
193 | /// Command to dial customer phone number
194 | ///
195 | public Command DialNumberCommand
196 | {
197 | get
198 | {
199 | return _DialNumberCommand ??
200 | (_DialNumberCommand = new Command(ExecuteDialNumberCommand));
201 | }
202 | }
203 |
204 | void ExecuteDialNumberCommand()
205 | {
206 | if (String.IsNullOrWhiteSpace(Customer.Phone))
207 | return;
208 |
209 | if (_CapabilityService.CanMakeCalls)
210 | {
211 | var phoneCallTask = MessagingPlugin.PhoneDialer;
212 | if (phoneCallTask.CanMakePhoneCall)
213 | phoneCallTask.MakePhoneCall(Customer.Phone.SanitizePhoneNumber());
214 | }
215 | else
216 | {
217 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
218 | {
219 | Title = "Simulator Not Supported",
220 | Message = "Phone calls are not supported in the iOS simulator.",
221 | Cancel = "OK"
222 | });
223 | }
224 | }
225 |
226 | Command _MessageNumberCommand;
227 |
228 | ///
229 | /// Command to message customer phone number
230 | ///
231 | public Command MessageNumberCommand
232 | {
233 | get
234 | {
235 | return _MessageNumberCommand ??
236 | (_MessageNumberCommand = new Command(ExecuteMessageNumberCommand));
237 | }
238 | }
239 |
240 | void ExecuteMessageNumberCommand()
241 | {
242 | if (String.IsNullOrWhiteSpace(Customer.Phone))
243 | return;
244 |
245 | if (_CapabilityService.CanSendMessages)
246 | {
247 | var messageTask = MessagingPlugin.SmsMessenger;
248 | if (messageTask.CanSendSms)
249 | messageTask.SendSms(Customer.Phone.SanitizePhoneNumber());
250 | }
251 | else
252 | {
253 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
254 | {
255 | Title = "Simulator Not Supported",
256 | Message = "Messaging is not supported in the iOS simulator.",
257 | Cancel = "OK"
258 | });
259 | }
260 | }
261 |
262 | Command _EmailCommand;
263 |
264 | ///
265 | /// Command to email customer
266 | ///
267 | public Command EmailCommand
268 | {
269 | get
270 | {
271 | return _EmailCommand ??
272 | (_EmailCommand = new Command(ExecuteEmailCommandCommand));
273 | }
274 | }
275 |
276 | void ExecuteEmailCommandCommand()
277 | {
278 | if (String.IsNullOrWhiteSpace(Customer.Email))
279 | return;
280 |
281 | if (_CapabilityService.CanSendEmail)
282 | {
283 | var emailTask = MessagingPlugin.EmailMessenger;
284 | if (emailTask.CanSendEmail)
285 | emailTask.SendEmail(Customer.Email);
286 | }
287 | else
288 | {
289 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
290 | {
291 | Title = "Simulator Not Supported",
292 | Message = "Email composition is not supported in the iOS simulator.",
293 | Cancel = "OK"
294 | });
295 | }
296 | }
297 |
298 | Command _GetDirectionsCommand;
299 |
300 | public Command GetDirectionsCommand
301 | {
302 | get
303 | {
304 | return _GetDirectionsCommand ??
305 | (_GetDirectionsCommand = new Command(async() =>
306 | await ExecuteGetDirectionsCommand()));
307 | }
308 | }
309 |
310 | async Task ExecuteGetDirectionsCommand()
311 | {
312 | var position = await GetPosition();
313 |
314 | var pin = new Pin() { Position = position };
315 |
316 | await CrossExternalMaps.Current.NavigateTo(pin.Label, pin.Position.Latitude, pin.Position.Longitude, NavigationType.Driving);
317 | }
318 |
319 | public void SetupMap()
320 | {
321 | if (HasAddress)
322 | {
323 | MessagingService.Current.SendMessage(MessageKeys.SetupMap);
324 | }
325 | }
326 |
327 | void DisplayGeocodingError()
328 | {
329 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
330 | {
331 | Title = "Geocoding Error",
332 | Message = "Please make sure the address is valid.",
333 | Cancel = "OK"
334 | });
335 |
336 | IsBusy = false;
337 | }
338 |
339 | public async Task GetPosition()
340 | {
341 | if (!HasAddress)
342 | return new Position(0, 0);
343 |
344 | IsBusy = true;
345 |
346 | Position p;
347 |
348 | p = (await _Geocoder.GetPositionsForAddressAsync(Customer.AddressString)).FirstOrDefault();
349 |
350 | // The Android geocoder (the underlying implementation in Android itself) fails with some addresses unless they're rounded to the hundreds.
351 | // So, this deals with that edge case.
352 | if (p.Latitude == 0 && p.Longitude == 0 && AddressBeginsWithNumber(Customer.AddressString))
353 | {
354 | var roundedAddress = GetAddressWithRoundedStreetNumber(Customer.AddressString);
355 |
356 | p = (await _Geocoder.GetPositionsForAddressAsync(roundedAddress)).FirstOrDefault();
357 | }
358 |
359 | IsBusy = false;
360 |
361 | return p;
362 | }
363 |
364 | ///
365 | /// Subscribes to "SaveCustomer" messages
366 | ///
367 | void SubscribeToSaveCustomerMessages()
368 | {
369 | // This subscribes to the "SaveCustomer" message
370 | MessagingService.Current.Subscribe(MessageKeys.SaveCustomer, (service, customer) =>
371 | {
372 | Customer = customer;
373 |
374 | // address has been updated, so we should update the map
375 | if (Customer.AddressString != _AddressString)
376 | {
377 | MessagingService.Current.SendMessage(MessageKeys.CustomerLocationUpdated, this.Customer);
378 |
379 | _AddressString = Customer.AddressString;
380 | }
381 | });
382 | }
383 |
384 | ///
385 | /// Subscribes to "CustomerLocationUpdated" messages
386 | ///
387 | void SubscribeToCustomerLocationUpdatedMessages()
388 | {
389 | // update the map when receiving the CustomerLocationUpdated message
390 | MessagingService.Current.Subscribe(MessageKeys.CustomerLocationUpdated, (service, customer) =>
391 | {
392 | OnPropertyChanged("HasAddress");
393 |
394 | SetupMap();
395 | });
396 | }
397 |
398 | static bool AddressBeginsWithNumber(string address)
399 | {
400 | return !String.IsNullOrWhiteSpace(address) && Char.IsDigit(address.ToCharArray().First());
401 | }
402 |
403 | static string GetAddressWithRoundedStreetNumber(string address)
404 | {
405 | var endingIndex = GetEndingIndexOfNumericPortionOfAddress(address);
406 |
407 | if (endingIndex == 0)
408 | return address;
409 |
410 | int originalNumber = 0;
411 | int roundedNumber = 0;
412 |
413 | Int32.TryParse(address.Substring(0, endingIndex + 1), out originalNumber);
414 |
415 | if (originalNumber == 0)
416 | return address;
417 |
418 | roundedNumber = originalNumber.RoundToLowestHundreds();
419 |
420 | return address.Replace(originalNumber.ToString(), roundedNumber.ToString());
421 | }
422 |
423 | static int GetEndingIndexOfNumericPortionOfAddress(string address)
424 | {
425 | int endingIndex = 0;
426 |
427 | for (int i = 0; i < address.Length; i++)
428 | {
429 | if (Char.IsDigit(address[i]))
430 | endingIndex = i;
431 | else
432 | break;
433 | }
434 |
435 | return endingIndex;
436 | }
437 | }
438 | }
439 |
440 |
--------------------------------------------------------------------------------
/Customers/ViewModels/CustomerListViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.ObjectModel;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using FormsToolkit;
6 | using Plugin.Messaging;
7 | using Xamarin.Forms;
8 |
9 | namespace Customers
10 | {
11 | public class CustomerListViewModel : BaseNavigationViewModel
12 | {
13 | public CustomerListViewModel()
14 | {
15 | _CapabilityService = DependencyService.Get();
16 |
17 | DataSource = new CustomerDataSource();
18 |
19 | SubscribeToSaveCustomerMessages();
20 |
21 | SubscribeToDeleteCustomerMessages();
22 | }
23 |
24 | // this is just a utility service that we're using in this demo app to mitigate some limitations of the iOS simulator
25 | ICapabilityService _CapabilityService;
26 |
27 | readonly IDataSource DataSource;
28 |
29 | ObservableCollection _Customers;
30 |
31 | Command _LoadCustomersCommand;
32 |
33 | Command _RefreshCustomersCommand;
34 |
35 | Command _NewCustomerCommand;
36 |
37 | public ObservableCollection Customers
38 | {
39 | get { return _Customers; }
40 | set
41 | {
42 | _Customers = value;
43 | OnPropertyChanged("Customers");
44 | }
45 | }
46 |
47 | ///
48 | /// Command to load customers
49 | ///
50 | public Command LoadCustomersCommand
51 | {
52 | get { return _LoadCustomersCommand ?? (_LoadCustomersCommand = new Command(async () => await ExecuteLoadCustomersCommand())); }
53 | }
54 |
55 | async Task ExecuteLoadCustomersCommand()
56 | {
57 | if (Customers == null)
58 | {
59 | LoadCustomersCommand.ChangeCanExecute();
60 |
61 | await FetchCustomers();
62 |
63 | LoadCustomersCommand.ChangeCanExecute();
64 | }
65 | }
66 |
67 | public Command RefreshCustomersCommand
68 | {
69 | get {
70 | return _RefreshCustomersCommand ?? (_RefreshCustomersCommand = new Command(async () => await ExecuteRefreshCustomersCommandCommand()));
71 | }
72 | }
73 |
74 | async Task ExecuteRefreshCustomersCommandCommand()
75 | {
76 | RefreshCustomersCommand.ChangeCanExecute();
77 |
78 | await FetchCustomers();
79 |
80 | RefreshCustomersCommand.ChangeCanExecute();
81 | }
82 |
83 | async Task FetchCustomers()
84 | {
85 | IsBusy = true;
86 |
87 | Customers = new ObservableCollection(await DataSource.GetItems(0, 1000));
88 |
89 | IsBusy = false;
90 | }
91 |
92 | ///
93 | /// Command to create new customer
94 | ///
95 | public Command NewCustomerCommand
96 | {
97 | get
98 | {
99 | return _NewCustomerCommand ??
100 | (_NewCustomerCommand = new Command(async () => await ExecuteNewCustomerCommand()));
101 | }
102 | }
103 |
104 | async Task ExecuteNewCustomerCommand()
105 | {
106 | await PushAsync(new CustomerEditPage() { BindingContext = new CustomerDetailViewModel(new Customer()) });
107 | }
108 |
109 | Command _DialNumberCommand;
110 |
111 | ///
112 | /// Command to dial customer phone number
113 | ///
114 | public Command DialNumberCommand
115 | {
116 | get
117 | {
118 | return _DialNumberCommand ??
119 | (_DialNumberCommand = new Command((parameter) =>
120 | ExecuteDialNumberCommand((string)parameter)));
121 | }
122 | }
123 |
124 | void ExecuteDialNumberCommand(string customerId)
125 | {
126 | if (String.IsNullOrWhiteSpace(customerId))
127 | return;
128 |
129 | var customer = _Customers.SingleOrDefault(c => c.Id == customerId);
130 |
131 | if (customer == null)
132 | return;
133 |
134 | if (_CapabilityService.CanMakeCalls)
135 | {
136 | var phoneCallTask = MessagingPlugin.PhoneDialer;
137 | if (phoneCallTask.CanMakePhoneCall)
138 | phoneCallTask.MakePhoneCall(customer.Phone.SanitizePhoneNumber());
139 | }
140 | else
141 | {
142 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
143 | {
144 | Title = "Simulator Not Supported",
145 | Message = "Phone calls are not supported in the iOS simulator.",
146 | Cancel = "OK"
147 | });
148 | }
149 | }
150 |
151 | Command _MessageNumberCommand;
152 |
153 | ///
154 | /// Command to message customer phone number
155 | ///
156 | public Command MessageNumberCommand
157 | {
158 | get
159 | {
160 | return _MessageNumberCommand ??
161 | (_MessageNumberCommand = new Command((parameter) =>
162 | ExecuteMessageNumberCommand((string)parameter)));
163 | }
164 | }
165 |
166 | void ExecuteMessageNumberCommand(string customerId)
167 | {
168 | if (String.IsNullOrWhiteSpace(customerId))
169 | return;
170 |
171 | var customer = _Customers.SingleOrDefault(c => c.Id == customerId);
172 |
173 | if (customer == null)
174 | return;
175 |
176 | if (_CapabilityService.CanSendMessages)
177 | {
178 | var messageTask = MessagingPlugin.SmsMessenger;
179 | if (messageTask.CanSendSms)
180 | messageTask.SendSms(customer.Phone.SanitizePhoneNumber());
181 | }
182 | else
183 | {
184 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
185 | {
186 | Title = "Simulator Not Supported",
187 | Message = "Messaging is not supported in the iOS simulator.",
188 | Cancel = "OK"
189 | });
190 | }
191 | }
192 |
193 | Command _EmailCommand;
194 |
195 | ///
196 | /// Command to email customer
197 | ///
198 | public Command EmailCommand
199 | {
200 | get
201 | {
202 | return _EmailCommand ??
203 | (_EmailCommand = new Command((parameter) =>
204 | ExecuteEmailCommandCommand((string)parameter)));
205 | }
206 | }
207 |
208 | void ExecuteEmailCommandCommand(string customerId)
209 | {
210 | if (String.IsNullOrWhiteSpace(customerId))
211 | return;
212 |
213 | var customer = _Customers.SingleOrDefault(c => c.Id == customerId);
214 |
215 | if (customer == null)
216 | return;
217 |
218 | if (_CapabilityService.CanSendEmail)
219 | {
220 | var emailTask = MessagingPlugin.EmailMessenger;
221 | if (emailTask.CanSendEmail)
222 | emailTask.SendEmail(customer.Email);
223 | }
224 | else
225 | {
226 | MessagingService.Current.SendMessage(MessageKeys.DisplayAlert, new MessagingServiceAlert()
227 | {
228 | Title = "Simulator Not Supported",
229 | Message = "Email composition is not supported in the iOS simulator.",
230 | Cancel = "OK"
231 | });
232 | }
233 | }
234 |
235 | ///
236 | /// Subscribes to "SaveCustomer" messages
237 | ///
238 | void SubscribeToSaveCustomerMessages()
239 | {
240 | // This subscribes to the "SaveCustomer" message, and then inserts or updates the customer accordingly
241 | MessagingService.Current.Subscribe(MessageKeys.SaveCustomer, async (service, customer) =>
242 | {
243 | IsBusy = true;
244 |
245 | await DataSource.SaveItem(customer);
246 |
247 | await FetchCustomers();
248 |
249 | IsBusy = false;
250 | });
251 | }
252 |
253 | ///
254 | /// Subscribes to "DeleteCustomer" messages
255 | ///
256 | void SubscribeToDeleteCustomerMessages()
257 | {
258 | // This subscribes to the "DeleteCustomer" message, and then deletes the customer accordingly
259 | MessagingService.Current.Subscribe(MessageKeys.DeleteCustomer, async (service, customer) =>
260 | {
261 | IsBusy = true;
262 |
263 | await DataSource.DeleteItem(customer.Id);
264 |
265 | await FetchCustomers();
266 |
267 | IsBusy = false;
268 | });
269 | }
270 | }
271 | }
272 |
273 |
--------------------------------------------------------------------------------
/Customers/Views/FloatingActionButtonView.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Xamarin.Forms;
3 |
4 | namespace Customers
5 | {
6 | public enum FloatingActionButtonSize
7 | {
8 | Normal,
9 | Mini
10 | }
11 |
12 | public class FloatingActionButtonView : View
13 | {
14 | public static readonly BindableProperty ImageNameProperty = BindableProperty.Create( p => p.ImageName, string.Empty);
15 | public string ImageName
16 | {
17 | get { return (string)GetValue (ImageNameProperty); }
18 | set { SetValue (ImageNameProperty, value); }
19 | }
20 |
21 | public static readonly BindableProperty ColorNormalProperty = BindableProperty.Create( p => p.ColorNormal, Color.White);
22 | public Color ColorNormal
23 | {
24 | get { return (Color)GetValue (ColorNormalProperty); }
25 | set { SetValue (ColorNormalProperty, value); }
26 | }
27 |
28 | public static readonly BindableProperty ColorPressedProperty = BindableProperty.Create( p => p.ColorPressed, Color.White);
29 | public Color ColorPressed
30 | {
31 | get { return (Color)GetValue (ColorPressedProperty); }
32 | set { SetValue (ColorPressedProperty, value); }
33 | }
34 |
35 | public static readonly BindableProperty ColorRippleProperty = BindableProperty.Create( p => p.ColorRipple, Color.White);
36 | public Color ColorRipple
37 | {
38 | get { return (Color)GetValue (ColorRippleProperty); }
39 | set { SetValue (ColorRippleProperty, value); }
40 | }
41 |
42 | public static readonly BindableProperty SizeProperty = BindableProperty.Create( p => p.Size, FloatingActionButtonSize.Normal);
43 | public FloatingActionButtonSize Size
44 | {
45 | get { return (FloatingActionButtonSize)GetValue (SizeProperty); }
46 | set { SetValue (SizeProperty, value); }
47 | }
48 |
49 | public static readonly BindableProperty HasShadowProperty = BindableProperty.Create( p => p.HasShadow, true);
50 | public bool HasShadow
51 | {
52 | get { return (bool)GetValue (HasShadowProperty); }
53 | set { SetValue (HasShadowProperty, value); }
54 | }
55 |
56 | public delegate void ShowHideDelegate(bool animate = true);
57 | public delegate void AttachToListViewDelegate(ListView listView);
58 |
59 | public ShowHideDelegate Show { get; set; }
60 | public ShowHideDelegate Hide { get; set; }
61 | public Action