From 8bd5a59754437ca7ffa6d0c21b0e41849177ed3a Mon Sep 17 00:00:00 2001 From: Ryan Thiele <39335530+RyanThiele@users.noreply.github.com> Date: Thu, 9 Aug 2018 12:51:58 -0400 Subject: [PATCH] Feature/correct logic (#2) * Added navigation to WPF application. * Added essential services. This folder is not to be removed. Services should not be edited. * Added Logging * Updated .NET framework reference to match the PCL. Finished startup logic. * Lowered WPF Project to 4.5.1. Removed Unused reference in Weather.Core. * Added an Edit ViewModel * Removed unused projects. * Added Unit test. * Added logic to search for location by 'City, State' or 'Postal Code' or 'Latitude, Longitude. Added unit test to test logic. --- .../AddLocationViewModelTests.vb | 157 +++++++++++ .../Fakes/Weather.Core.fakes | Bin 0 -> 204 bytes .../My Project/Application.Designer.vb | 13 + .../My Project/Application.myapp | 10 + .../My Project/AssemblyInfo.vb | 35 +++ .../My Project/Resources.Designer.vb | 63 +++++ .../My Project/Resources.resx | 117 ++++++++ .../My Project/Settings.Designer.vb | 73 +++++ .../My Project/Settings.settings | 7 + .../Weather.Core.Tests/UnitTestBase.vb | 16 ++ .../Weather.Core.Tests.vbproj | 163 +++++++++++ Weather.Universal/Weather.Core/Extensions.vb | 132 +++++++++ .../Weather.Core/Models/GeoCoordinate.vb | 4 +- .../Essential Services/IDialogService.vb | 8 + .../Essential Services/INavigationService.vb | 15 + .../Weather.Core/Services/ILocationService.vb | 3 +- .../Weather.Core/Services/ISettingsService.vb | 2 +- .../ViewModels/AddLocationViewModel.vb | 259 ++++++++++++++++++ .../CurrentObservationsViewModel.vb | 4 +- .../ViewModels/LocationViewModel.vb | 13 +- .../Weather.Core/ViewModels/MainViewModel.vb | 135 ++++++--- .../ViewModels/WeatherSourcesViewModel.vb | 2 +- .../ViewModels/_EditViewModelBase.vb | 113 ++++++++ .../Weather.Core/ViewModels/_ViewModelBase.vb | 42 +++ .../Weather.Core/Weather.Core.vbproj | 27 +- .../Weather.Core/packages.config | 38 +++ .../Weather.Services.Noaa/WeatherService.vb | 3 +- Weather.Universal/Weather.Universal.sln | 164 ++++++----- Weather.Universal/Weather.WPF/App.config | 38 ++- .../Weather.WPF/Application.xaml | 12 +- .../Weather.WPF/Application.xaml.vb | 31 ++- .../Weather.WPF/DataTemplates.xaml | 10 + .../AddWeatherSourceViewModelSampleData.xaml | 2 +- .../Weather.WPF/Services/LocationService.vb | 6 +- .../Weather.WPF/Services/NavigationService.vb | 2 +- .../Weather.WPF/Services/SettingsService.vb | 16 +- .../Weather.WPF/Views/AddLocationView.xaml | 23 ++ .../Weather.WPF/Views/AddLocationView.xaml.vb | 3 + .../Views/CurrentObservationsView.xaml | 2 +- .../Weather.WPF/Weather.WPF.vbproj | 121 +++----- Weather.Universal/Weather.WPF/packages.config | 89 +++--- 41 files changed, 1666 insertions(+), 307 deletions(-) create mode 100644 Weather.Universal/Weather.Core.Tests/AddLocationViewModelTests.vb create mode 100644 Weather.Universal/Weather.Core.Tests/Fakes/Weather.Core.fakes create mode 100644 Weather.Universal/Weather.Core.Tests/My Project/Application.Designer.vb create mode 100644 Weather.Universal/Weather.Core.Tests/My Project/Application.myapp create mode 100644 Weather.Universal/Weather.Core.Tests/My Project/AssemblyInfo.vb create mode 100644 Weather.Universal/Weather.Core.Tests/My Project/Resources.Designer.vb create mode 100644 Weather.Universal/Weather.Core.Tests/My Project/Resources.resx create mode 100644 Weather.Universal/Weather.Core.Tests/My Project/Settings.Designer.vb create mode 100644 Weather.Universal/Weather.Core.Tests/My Project/Settings.settings create mode 100644 Weather.Universal/Weather.Core.Tests/UnitTestBase.vb create mode 100644 Weather.Universal/Weather.Core.Tests/Weather.Core.Tests.vbproj create mode 100644 Weather.Universal/Weather.Core/Extensions.vb create mode 100644 Weather.Universal/Weather.Core/Services/Essential Services/IDialogService.vb create mode 100644 Weather.Universal/Weather.Core/Services/Essential Services/INavigationService.vb create mode 100644 Weather.Universal/Weather.Core/ViewModels/AddLocationViewModel.vb create mode 100644 Weather.Universal/Weather.Core/ViewModels/_EditViewModelBase.vb create mode 100644 Weather.Universal/Weather.Core/packages.config create mode 100644 Weather.Universal/Weather.WPF/DataTemplates.xaml create mode 100644 Weather.Universal/Weather.WPF/Views/AddLocationView.xaml create mode 100644 Weather.Universal/Weather.WPF/Views/AddLocationView.xaml.vb diff --git a/Weather.Universal/Weather.Core.Tests/AddLocationViewModelTests.vb b/Weather.Universal/Weather.Core.Tests/AddLocationViewModelTests.vb new file mode 100644 index 0000000..2f03e8a --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/AddLocationViewModelTests.vb @@ -0,0 +1,157 @@ +Imports System.Text +Imports Microsoft.VisualStudio.TestTools.UnitTesting + + +Public Class AddLocationViewModelTests + Inherits UnitTestBase + + + + Public Sub EmptySearchString_ShouldNotPerformSearch() + ' Prepare + Dim isUsingLocationService As Boolean = False + Dim locationService As New Services.Fakes.StubILocationService + With locationService + .GetLocationByCityAndStateAsyncStringStringInt32CancellationToken = Async Function() + isUsingLocationService = True + Await Task.Delay(0) + Return Nothing + End Function + .GetLocationByLatitudeLongitudeAsyncDoubleDoubleInt32CancellationToken = Async Function() + isUsingLocationService = True + Await Task.Delay(0) + Return Nothing + End Function + .GetLocationByPostalCodeAsyncStringInt32CancellationToken = Async Function() + isUsingLocationService = True + Await Task.Delay(0) + Return Nothing + End Function + End With + Dim viewModel As New ViewModels.AddLocationViewModel(CreateMessageBus, CreateDialogService, CreateNavigationService, locationService) + + ' Execute + viewModel.SearchCommand.Execute(Nothing) + + ' Assert + Assert.IsFalse(isUsingLocationService, "AddLocationViewModel is still trying to search when an empty search string.") + End Sub + + + Public Sub USZipCodeSearchString_ShouldPerformSearchWithPostalCodeQuery() + ' Prepare + Dim isUsingLocationServiceByPostalCode As Boolean = False + Dim isUsingLocationServiceByCityState As Boolean = False + Dim isUsingLocationServiceByLatLon As Boolean = False + Dim locationService As New Services.Fakes.StubILocationService + With locationService + .GetLocationByCityAndStateAsyncStringStringInt32CancellationToken = Async Function() + isUsingLocationServiceByCityState = True + Await Task.Delay(0) + Return Nothing + End Function + + .GetLocationByLatitudeLongitudeAsyncDoubleDoubleInt32CancellationToken = Async Function() + isUsingLocationServiceByLatLon = True + Await Task.Delay(0) + Return Nothing + End Function + + .GetLocationByPostalCodeAsyncStringInt32CancellationToken = Async Function() + isUsingLocationServiceByPostalCode = True + Await Task.Delay(0) + Return Nothing + End Function + End With + Dim viewModel As New ViewModels.AddLocationViewModel(CreateMessageBus, CreateDialogService, CreateNavigationService, locationService) + viewModel.SearchString = "12345" + + ' Execute + viewModel.SearchCommand.Execute(Nothing) + + ' Assert + Assert.IsFalse(isUsingLocationServiceByCityState, "AddLocationViewModel is using City/State when using a US Postal Code search string.") + Assert.IsFalse(isUsingLocationServiceByLatLon, "AddLocationViewModel is using Latitude/Longitude when using a US Postal Code search string.") + Assert.IsTrue(isUsingLocationServiceByPostalCode, "AddLocationViewModel is not using Postal Code when using a US Postal Code search string.") + + End Sub + + + Public Sub CityStateSearchString_ShouldPerformSearchWithPostalCodeQuery() + ' Prepare + Dim isUsingLocationServiceByPostalCode As Boolean = False + Dim isUsingLocationServiceByCityState As Boolean = False + Dim isUsingLocationServiceByLatLon As Boolean = False + Dim locationService As New Services.Fakes.StubILocationService + With locationService + .GetLocationByCityAndStateAsyncStringStringInt32CancellationToken = Async Function() + isUsingLocationServiceByCityState = True + Await Task.Delay(0) + Return Nothing + End Function + + .GetLocationByLatitudeLongitudeAsyncDoubleDoubleInt32CancellationToken = Async Function() + isUsingLocationServiceByLatLon = True + Await Task.Delay(0) + Return Nothing + End Function + + .GetLocationByPostalCodeAsyncStringInt32CancellationToken = Async Function() + isUsingLocationServiceByPostalCode = True + Await Task.Delay(0) + Return Nothing + End Function + End With + + Dim viewModel As New ViewModels.AddLocationViewModel(CreateMessageBus, CreateDialogService, CreateNavigationService, locationService) + viewModel.SearchString = "City, State" + + ' Execute + viewModel.SearchCommand.Execute(Nothing) + + ' Assert + Assert.IsTrue(isUsingLocationServiceByCityState, "AddLocationViewModel is not using City/State when using a , search string.") + Assert.IsFalse(isUsingLocationServiceByLatLon, "AddLocationViewModel is using Latitude/Longitude when using a , search string.") + Assert.IsFalse(isUsingLocationServiceByPostalCode, "AddLocationViewModel is using Postal Code when using a , search string.") + End Sub + + + Public Sub LatLonSearchString_ShouldPerformSearchWithPostalCodeQuery() + ' Prepare + Dim isUsingLocationServiceByPostalCode As Boolean = False + Dim isUsingLocationServiceByCityState As Boolean = False + Dim isUsingLocationServiceByLatLon As Boolean = False + Dim locationService As New Services.Fakes.StubILocationService + With locationService + .GetLocationByCityAndStateAsyncStringStringInt32CancellationToken = Async Function() + isUsingLocationServiceByCityState = True + Await Task.Delay(0) + Return Nothing + End Function + + .GetLocationByLatitudeLongitudeAsyncDoubleDoubleInt32CancellationToken = Async Function() + isUsingLocationServiceByLatLon = True + Await Task.Delay(0) + Return Nothing + End Function + + .GetLocationByPostalCodeAsyncStringInt32CancellationToken = Async Function() + isUsingLocationServiceByPostalCode = True + Await Task.Delay(0) + Return Nothing + End Function + End With + Dim viewModel As New ViewModels.AddLocationViewModel(CreateMessageBus, CreateDialogService, CreateNavigationService, locationService) + viewModel.SearchString = "123.45, 123.45" + + ' Execute + viewModel.SearchCommand.Execute(Nothing) + + ' Assert + Assert.IsFalse(isUsingLocationServiceByCityState, "AddLocationViewModel is using City/State when using a , search string.") + Assert.IsTrue(isUsingLocationServiceByLatLon, "AddLocationViewModel is not using Latitude/Longitude when using a , search string.") + Assert.IsFalse(isUsingLocationServiceByPostalCode, "AddLocationViewModel is using Postal Code when using a , search string.") + End Sub + + +End Class \ No newline at end of file diff --git a/Weather.Universal/Weather.Core.Tests/Fakes/Weather.Core.fakes b/Weather.Universal/Weather.Core.Tests/Fakes/Weather.Core.fakes new file mode 100644 index 0000000000000000000000000000000000000000..2cb31d5b4cba8bac2120e37dd228d199a3fb2529 GIT binary patch literal 204 zcmYL@u?oUa5Co?d{D+Va$Wu#s21Ky5wSG!O(U_1}=+CPYMdXUxTkdA>^^B}ZoT=!^ zxKnfCs$P(D +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + diff --git a/Weather.Universal/Weather.Core.Tests/My Project/Application.myapp b/Weather.Universal/Weather.Core.Tests/My Project/Application.myapp new file mode 100644 index 0000000..758895d --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/My Project/Application.myapp @@ -0,0 +1,10 @@ + + + false + false + 0 + true + 0 + 1 + true + diff --git a/Weather.Universal/Weather.Core.Tests/My Project/AssemblyInfo.vb b/Weather.Universal/Weather.Core.Tests/My Project/AssemblyInfo.vb new file mode 100644 index 0000000..90955be --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/My Project/AssemblyInfo.vb @@ -0,0 +1,35 @@ +Imports System +Imports System.Reflection +Imports System.Runtime.InteropServices + +' General Information about an assembly is controlled through the following +' set of attributes. Change these attribute values to modify the information +' associated with an assembly. + +' Review the values of the assembly attributes + + + + + + + + + + +'The following GUID is for the ID of the typelib if this project is exposed to COM + + +' Version information for an assembly consists of the following four values: +' +' Major Version +' Minor Version +' Build Number +' Revision +' +' You can specify all the values or you can default the Build and Revision Numbers +' by using the '*' as shown below: +' + + + diff --git a/Weather.Universal/Weather.Core.Tests/My Project/Resources.Designer.vb b/Weather.Universal/Weather.Core.Tests/My Project/Resources.Designer.vb new file mode 100644 index 0000000..e9dee63 --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/My Project/Resources.Designer.vb @@ -0,0 +1,63 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + +Imports System + +Namespace My.Resources + + 'This class was auto-generated by the StronglyTypedResourceBuilder + 'class via a tool like ResGen or Visual Studio. + 'To add or remove a member, edit your .ResX file then rerun ResGen + 'with the /str option, or rebuild your VS project. + ''' + ''' A strongly-typed resource class, for looking up localized strings, etc. + ''' + _ + Friend Module Resources + + Private resourceMan As Global.System.Resources.ResourceManager + + Private resourceCulture As Global.System.Globalization.CultureInfo + + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + _ + Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager + Get + If Object.ReferenceEquals(resourceMan, Nothing) Then + Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("Weather.Tests.Resources", GetType(Resources).Assembly) + resourceMan = temp + End If + Return resourceMan + End Get + End Property + + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + _ + Friend Property Culture() As Global.System.Globalization.CultureInfo + Get + Return resourceCulture + End Get + Set + resourceCulture = value + End Set + End Property + End Module +End Namespace diff --git a/Weather.Universal/Weather.Core.Tests/My Project/Resources.resx b/Weather.Universal/Weather.Core.Tests/My Project/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/My Project/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Weather.Universal/Weather.Core.Tests/My Project/Settings.Designer.vb b/Weather.Universal/Weather.Core.Tests/My Project/Settings.Designer.vb new file mode 100644 index 0000000..97b48af --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/My Project/Settings.Designer.vb @@ -0,0 +1,73 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.42000 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + + +Namespace My + + _ + Partial Friend NotInheritable Class MySettings + Inherits Global.System.Configuration.ApplicationSettingsBase + + Private Shared defaultInstance As MySettings = CType(Global.System.Configuration.ApplicationSettingsBase.Synchronized(New MySettings()),MySettings) + +#Region "My.Settings Auto-Save Functionality" +#If _MyType = "WindowsForms" Then + Private Shared addedHandler As Boolean + + Private Shared addedHandlerLockObject As New Object + + _ + Private Shared Sub AutoSaveSettings(ByVal sender As Global.System.Object, ByVal e As Global.System.EventArgs) + If My.Application.SaveMySettingsOnExit Then + My.Settings.Save() + End If + End Sub +#End If +#End Region + + Public Shared ReadOnly Property [Default]() As MySettings + Get + +#If _MyType = "WindowsForms" Then + If Not addedHandler Then + SyncLock addedHandlerLockObject + If Not addedHandler Then + AddHandler My.Application.Shutdown, AddressOf AutoSaveSettings + addedHandler = True + End If + End SyncLock + End If +#End If + Return defaultInstance + End Get + End Property + End Class +End Namespace + +Namespace My + + _ + Friend Module MySettingsProperty + + _ + Friend ReadOnly Property Settings() As Global.Weather.Tests.My.MySettings + Get + Return Global.Weather.Tests.My.MySettings.Default + End Get + End Property + End Module +End Namespace diff --git a/Weather.Universal/Weather.Core.Tests/My Project/Settings.settings b/Weather.Universal/Weather.Core.Tests/My Project/Settings.settings new file mode 100644 index 0000000..85b890b --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/My Project/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Weather.Universal/Weather.Core.Tests/UnitTestBase.vb b/Weather.Universal/Weather.Core.Tests/UnitTestBase.vb new file mode 100644 index 0000000..d4408d8 --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/UnitTestBase.vb @@ -0,0 +1,16 @@ + + Public MustInherit Class UnitTestBase + + Protected Function CreateMessageBus() As IMessageBus + Return New Fakes.StubIMessageBus + End Function + + Protected Function CreateDialogService() As Services.IDialogService + Return New Services.Fakes.StubIDialogService + End Function + + Protected Function CreateNavigationService() As Services.INavigationService + Return New Services.Fakes.StubINavigationService + End Function + +End Class diff --git a/Weather.Universal/Weather.Core.Tests/Weather.Core.Tests.vbproj b/Weather.Universal/Weather.Core.Tests/Weather.Core.Tests.vbproj new file mode 100644 index 0000000..1b84bd1 --- /dev/null +++ b/Weather.Universal/Weather.Core.Tests/Weather.Core.Tests.vbproj @@ -0,0 +1,163 @@ + + + + Debug + AnyCPU + {949D409C-7334-4448-81F1-3FE228C4968F} + Library + Weather.Tests + Weather.Core.Tests + 512 + Windows + v4.5.1 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{F184B08F-C81C-45F6-A57F-5ABD9991F28F} + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + true + full + true + true + bin\Debug\ + Weather.Core.Tests.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + pdbonly + false + true + true + bin\Release\ + Weather.Core.Tests.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + On + + + Binary + + + Off + + + On + + + + False + + + + FakesAssemblies\System.Core.4.0.0.0.Fakes.dll + + + + + + + + FakesAssemblies\Weather.Core.Fakes.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + Application.myapp + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + + VbMyResourcesResXFileCodeGenerator + Resources.Designer.vb + My.Resources + Designer + + + + + + MyApplicationCodeGenerator + Application.Designer.vb + + + SettingsSingleFileGenerator + My + Settings.Designer.vb + + + + + {9e2c42c1-7a4b-4ee4-8236-112dee976f80} + Weather.Core + + + + + + + False + + + False + + + False + + + False + + + + + + + + \ No newline at end of file diff --git a/Weather.Universal/Weather.Core/Extensions.vb b/Weather.Universal/Weather.Core/Extensions.vb new file mode 100644 index 0000000..8825287 --- /dev/null +++ b/Weather.Universal/Weather.Core/Extensions.vb @@ -0,0 +1,132 @@ +Imports System.Runtime.CompilerServices +Imports System.Xml.Linq +Imports System.Globalization +Imports System.Xml +Imports System.Text.RegularExpressions + +Friend Module Extensions + + Public Structure CityAndRegion + Dim City As String + Dim Region As String + End Structure + + Public Structure LatLong + Dim Latitude As Double + Dim Longitude As Double + End Structure + + Public Enum Countries + UnitesStatesOfAmerica + UnitedKingdom + India + Canada + End Enum + + + Public Function ToDouble(s As String, Optional defaultValue As Double = 0) As Double + Dim value As Double = defaultValue + If Not String.IsNullOrWhiteSpace(s) Then Double.TryParse(s, value) + Return value + End Function + + + Public Function ToDecimal(s As String, Optional defaultValue As Decimal = 0) As Decimal + Dim value As Decimal = defaultValue + If Not String.IsNullOrWhiteSpace(s) Then Decimal.TryParse(s, value) + Return value + End Function + + + Public Function ToInteger(s As String, Optional defaultValue As Integer = 0) As Integer + Dim value As Integer = defaultValue + If Not String.IsNullOrWhiteSpace(s) Then Integer.TryParse(s, value) + Return value + End Function + + + Public Function ValueIfExists(element As XElement) As String + If element IsNot Nothing Then + Return element.Value + Else + Return Nothing + End If + End Function + + + Public Function ValueIfExists(attribute As XAttribute) As String + If attribute IsNot Nothing Then + Return attribute.Value + Else + Return Nothing + End If + End Function + + ' + 'Public Function FromXmlDateTimeToDateTime(s As String, Optional defaultValue As DateTime? = Nothing) As DateTime + ' If Not defaultValue.HasValue Then defaultValue = New DateTime + ' Dim value As DateTime? = XmlConvert.ToDateTime(s, XmlDateTimeSerializationMode.Local) + ' If value Is Nothing Then value = defaultValue.Value + ' Return value + 'End Function + + + Public Function FromRfc22StringToDateTime(s As String, Optional defaultValue As DateTime? = Nothing) As DateTime + Dim provider As CultureInfo = CultureInfo.InvariantCulture + ' Sun, 08 Jul 2018 02:55:00 -0400 + ' Sat, 07 Jul 2018 17:48:00 -0400 + Return DateTime.ParseExact(s, "ddd, dd MMM yyyy HH:mm:ss zzz", provider) + End Function + + + Public Function PostalCodeRegionFromPostalCode(s As String) As Countries? + If Regex.IsMatch(s, "^\d{3}\s?\d{3}$") Then + 'Indian style pincodes / postal codes used by the Indian postal departments which are 6 digits long and may have space after the 3rd digit + Return Countries.India + ElseIf Regex.IsMatch(s, "^[A-Za-z]{1,2}[\d]{1,2}([A-Za-z])?\s?[\d][A-Za-z]{2}$") Then + ' UK Postal Codes + Return Countries.UnitedKingdom + ElseIf Regex.IsMatch(s, "^([0-9]{5})(?:[-\s]*([0-9]{4}))?$") Then + ' US Postal Codes + Return Countries.UnitesStatesOfAmerica + ElseIf Regex.IsMatch(s, "^([A-Z][0-9][A-Z])\s*([0-9][A-Z][0-9])$") Then + ' Canada + Return Countries.Canada + Else + Return Nothing + End If + + End Function + + + + Public Function ToLatLong(s As String) As LatLong? + If Not s.Contains(","c) Then Return Nothing + + Dim parts() As String = s.Split(","c) + If parts.Length < 2 Then Return Nothing + + Dim lat As Double = 0 + Dim lon As Double = 0 + + Double.TryParse(parts(0), lat) + Double.TryParse(parts(1), lon) + + If lat = 0 And lon = 0 Then + Return Nothing + Else + Return New LatLong() With {.Latitude = lat, .Longitude = lon} + End If + End Function + + + Public Function ToCityAndRegion(s As String) As CityAndRegion? + If Not s.Contains(","c) Then Return Nothing + + Dim parts() As String = s.Split(","c) + If parts.Length < 2 Then Return Nothing + + Return New CityAndRegion() With {.City = parts(0), .Region = parts(1)} + End Function + +End Module diff --git a/Weather.Universal/Weather.Core/Models/GeoCoordinate.vb b/Weather.Universal/Weather.Core/Models/GeoCoordinate.vb index db7c440..134aecf 100644 --- a/Weather.Universal/Weather.Core/Models/GeoCoordinate.vb +++ b/Weather.Universal/Weather.Core/Models/GeoCoordinate.vb @@ -9,12 +9,12 @@ ''' The latitude of the geo coordinate ''' ''' - Public Property Latitude As Decimal + Public Property Latitude As Double ''' ''' The Longitude of the geo coordinate ''' - Public Property Longitude As Decimal + Public Property Longitude As Double End Class End Namespace diff --git a/Weather.Universal/Weather.Core/Services/Essential Services/IDialogService.vb b/Weather.Universal/Weather.Core/Services/Essential Services/IDialogService.vb new file mode 100644 index 0000000..0a2ed99 --- /dev/null +++ b/Weather.Universal/Weather.Core/Services/Essential Services/IDialogService.vb @@ -0,0 +1,8 @@ +Namespace Services + + Public Interface IDialogService + Function ShowYesNoDialog(title As String, message As String) As Boolean + Function ShowOkDialog(title As String, message As String) As Boolean + End Interface + +End Namespace diff --git a/Weather.Universal/Weather.Core/Services/Essential Services/INavigationService.vb b/Weather.Universal/Weather.Core/Services/Essential Services/INavigationService.vb new file mode 100644 index 0000000..17d1a6c --- /dev/null +++ b/Weather.Universal/Weather.Core/Services/Essential Services/INavigationService.vb @@ -0,0 +1,15 @@ +Imports System.Threading.Tasks +Imports Weather.ViewModels + +Namespace Services + + Public Interface INavigationService + Sub NavigatePrevious() + Sub NavigateNext() + Sub NavigateTo(Of TViewModel As ViewModelBase)(Optional addToHistory As Boolean = True) + + Sub RemoveLastFromBackStack() + Sub RemoveBackStack() + End Interface + +End Namespace diff --git a/Weather.Universal/Weather.Core/Services/ILocationService.vb b/Weather.Universal/Weather.Core/Services/ILocationService.vb index aa21f54..d549f77 100644 --- a/Weather.Universal/Weather.Core/Services/ILocationService.vb +++ b/Weather.Universal/Weather.Core/Services/ILocationService.vb @@ -4,7 +4,8 @@ Namespace Services Public Interface ILocationService Function GetLocationByPostalCodeAsync(postalCode As String, numberOfStations As Integer, token As CancellationToken) As Task(Of Models.Location) - Function GetLocationByLatitudeLongitudeAsync(latitude As Decimal, longitude As Decimal, numberOfStations As Integer, token As CancellationToken) As Task(Of Models.Location) + Function GetLocationByCityAndStateAsync(city As String, state As String, numberOfStations As Integer, token As CancellationToken) As Task(Of Models.Location) + Function GetLocationByLatitudeLongitudeAsync(latitude As Double, longitude As Double, numberOfStations As Integer, token As CancellationToken) As Task(Of Models.Location) End Interface End Namespace diff --git a/Weather.Universal/Weather.Core/Services/ISettingsService.vb b/Weather.Universal/Weather.Core/Services/ISettingsService.vb index 141da18..beea892 100644 --- a/Weather.Universal/Weather.Core/Services/ISettingsService.vb +++ b/Weather.Universal/Weather.Core/Services/ISettingsService.vb @@ -8,7 +8,7 @@ Namespace Services Function RefreshLocationsAsync(deleteExisting As Boolean) As Task Function RefreshStationsAsync(deleteExisting As Boolean) As Task Function SetSelectedLocationsAsync(locations As IEnumerable(Of Models.Location)) As Task - Function GetSelectedLocationsAsync() As Task(Of IEnumerable(Of Models.Location)) + Function GetLocationsAsync(token As CancellationToken) As Task(Of IEnumerable(Of Models.Location)) Function GetCurrentLocationAsync(token As CancellationToken) As Task(Of Models.GeoCoordinate) End Interface diff --git a/Weather.Universal/Weather.Core/ViewModels/AddLocationViewModel.vb b/Weather.Universal/Weather.Core/ViewModels/AddLocationViewModel.vb new file mode 100644 index 0000000..fe2d762 --- /dev/null +++ b/Weather.Universal/Weather.Core/ViewModels/AddLocationViewModel.vb @@ -0,0 +1,259 @@ +Imports System.Threading +Imports System.Windows.Input +Imports Weather.Services +Imports Microsoft.VisualBasic +Imports System.Text.RegularExpressions + +Namespace ViewModels + + Public Class AddLocationViewModel + Inherits EditViewModelBase + + Private ReadOnly _messageBus As IMessageBus + Private ReadOnly _dialogService As IDialogService + Private ReadOnly _navigationService As INavigationService + 'Private ReadOnly _weatherService As IWeatherService + 'Private ReadOnly _settingsService As ISettingsService + + Private ReadOnly _locationService As ILocationService + Private _searchCancelTokenSource As CancellationTokenSource + + Private Const EMPTY_SEARCH_STRING_ERROR_MSG As String = "There must be some place you want to search!" + +#Region "Constructors" + + Public Sub New() + + End Sub + + Public Sub New(messageBus As IMessageBus, + dialogService As IDialogService, + navigationService As INavigationService, + locationService As ILocationService) + + _messageBus = messageBus + _dialogService = dialogService + _navigationService = navigationService + _locationService = locationService + '_weatherService = weatherService + '_settingsService = settingsService + +#If DEBUG Then + PostalCode = "46845" +#End If + End Sub + +#End Region + +#Region "Properties" + +#Region "PostalCode" + + Dim _PostalCode As String + Public Property PostalCode As String + Get + Return _PostalCode + End Get + Set(value As String) + _PostalCode = value + OnPropertyChanged("PostalCode") + End Set + End Property + +#End Region + +#Region "SearchString" + + Dim _SearchString As String + Public Property SearchString As String + Get + Return _SearchString + End Get + Set + _SearchString = Value + OnPropertyChanged("SearchString") + End Set + End Property + +#End Region + +#Region "IsSearching" + + Dim _IsSearching As Boolean + Public Property IsSearching As Boolean + Get + Return _IsSearching + End Get + Set(value As Boolean) + _IsSearching = value + OnPropertyChanged("IsSearching") + End Set + End Property + +#End Region + +#End Region + +#Region "Commands" + +#Region "SearchCommand" + + Dim _SearchCommand As ICommand + Public ReadOnly Property SearchCommand As ICommand + Get + If _SearchCommand Is Nothing Then + _SearchCommand = New Commands.RelayCommand(AddressOf ExecuteSearch, AddressOf CanExecuteSearch) + End If + + Return _SearchCommand + End Get + End Property + + Private Function CanExecuteSearch() As Boolean + Return Not String.IsNullOrWhiteSpace(_SearchString) + End Function + + Private Async Sub ExecuteSearch() + IsSearching = True + Await PerformSearchAsync() + IsSearching = False + + 'Try + ' Status = "Searching..." + ' If String.IsNullOrWhiteSpace(SearchString) Then + ' Return + ' End If + + ' ' search for the location + ' _searchCancelTokenSource = New CancellationTokenSource + ' Dim location As Models.Location = Await _locationService.GetLocationByPostalCodeAsync(PostalCode, 3, _searchCancelTokenSource.Token) + + ' ' location is not found. notify user and bail. + ' If location Is Nothing Then + ' Status = "Could not find a weather station for postal code: " & PostalCode & ". Please try again." + ' Return + ' End If + + ' ' check if location already in cache. + ' Dim locations As IEnumerable(Of Models.Location) = Await _settingsService.GetLocationsAsync(New CancellationToken) + ' If locations IsNot Nothing Then + ' Dim existingLocation As Models.Location = locations.Where(Function(o) o.Address.PostalCode.Equals(PostalCode)).SingleOrDefault + ' ' location already exits, notify user and bail. + ' If existingLocation IsNot Nothing Then + ' Status = PostalCode & " already exists. Please try again." + ' Return + ' End If + ' End If + + ' ' if we get to here, all conditions are met to enter the location. + ' ' ask the user if they want to add the location. + ' If _dialogService.ShowYesNoDialog("Weather Station Found!", "Found a station for '" & PostalCode & ": " & location.WeatherStations.First.Name & ". Do you want to use this station?") Then + ' Dim locationList As New List(Of Models.Location) + ' If locations IsNot Nothing Then locationList = New List(Of Models.Location)(locations) + ' locationList.Add(location) + ' Await _settingsService.SetSelectedLocationsAsync(locationList) + ' End If + + ' Status = "Added " & PostalCode & " to cache." + + 'Catch ex As Exception + ' Status = "There was a problem with the search: " & ex.Message + 'End Try + + End Sub + +#End Region + +#Region "CancelCommand" + Dim _CancelCommand As ICommand + Public ReadOnly Property CancelCommand As ICommand + Get + If _CancelCommand Is Nothing Then + _CancelCommand = New Commands.RelayCommand(AddressOf ExecuteCancel, AddressOf CanExecuteCancel) + End If + + Return _CancelCommand + End Get + End Property + + Private Function CanExecuteCancel() As Boolean + Return True + End Function + + Private Sub ExecuteCancel() + If Not IsSearching Then Return + If _searchCancelTokenSource Is Nothing OrElse Not _searchCancelTokenSource.IsCancellationRequested Then Return + _searchCancelTokenSource.Cancel() + End Sub + +#End Region + + +#End Region + +#Region "Methods" + + Private Function CheckSearchTokenStatus() As Boolean + If _searchCancelTokenSource.IsCancellationRequested Then + Status = "User canceled the search." + End If + + Return _searchCancelTokenSource.IsCancellationRequested + End Function + + Private Async Function PerformSearchAsync() As Task + ' Sanity check + If String.IsNullOrWhiteSpace(_SearchString) Then + AddError(EMPTY_SEARCH_STRING_ERROR_MSG, "SearchString") + Return + Else + RemoveError(EMPTY_SEARCH_STRING_ERROR_MSG, "SearchString") + End If + + SearchString = SearchString.Trim + + ' Determine what the user is searching for. + _cancellationTokenSource = New CancellationTokenSource + + ' Get the Location. + Dim location As Models.Location = Await GetLocationAsync() + If location Is Nothing Then + ' location is not found. notify user and bail. + Status = "Could not find a weather station for postal code: " & PostalCode & ". Please try again." + Return + End If + + If _dialogService.ShowYesNoDialog("Location Found!", "There was a location found: " & location.Address.DisplayString & Environment.NewLine & + "Do you want to use this location?") Then + Dim locationList As New List(Of Models.Location) + End If + End Function + + Private Async Function GetLocationAsync() As Task(Of Models.Location) + Dim location As Models.Location = Nothing + + Dim postalCodeCountry As Countries? = SearchString.PostalCodeRegionFromPostalCode + If postalCodeCountry.HasValue Then + location = Await _locationService.GetLocationByPostalCodeAsync(SearchString, 3, _cancellationTokenSource.Token) + Return location + End If + + Dim latLong As LatLong? = SearchString.ToLatLong + If latLong.HasValue Then + location = Await _locationService.GetLocationByLatitudeLongitudeAsync(latLong.Value.Latitude, latLong.Value.Longitude, 3, _cancellationTokenSource.Token) + Return location + End If + + Dim cityAndRegion As CityAndRegion? = SearchString.ToCityAndRegion + If cityAndRegion.HasValue Then + location = Await _locationService.GetLocationByCityAndStateAsync(cityAndRegion.Value.City, cityAndRegion.Value.Region, 3, _cancellationTokenSource.Token) + Return location + End If + + Return location + End Function +#End Region + + End Class + +End Namespace diff --git a/Weather.Universal/Weather.Core/ViewModels/CurrentObservationsViewModel.vb b/Weather.Universal/Weather.Core/ViewModels/CurrentObservationsViewModel.vb index 290dc40..c09c3c8 100644 --- a/Weather.Universal/Weather.Core/ViewModels/CurrentObservationsViewModel.vb +++ b/Weather.Universal/Weather.Core/ViewModels/CurrentObservationsViewModel.vb @@ -35,11 +35,11 @@ Namespace ViewModels #Region "IsUsingImperial" Dim _IsUsingImperial As Boolean - Public Property IsUsingImperial As String + Public Property IsUsingImperial As Boolean Get Return _IsUsingImperial End Get - Set(value As String) + Set(value As Boolean) _IsUsingImperial = value OnPropertyChanged("IsUsingImperial") End Set diff --git a/Weather.Universal/Weather.Core/ViewModels/LocationViewModel.vb b/Weather.Universal/Weather.Core/ViewModels/LocationViewModel.vb index ab49786..b3a3ffd 100644 --- a/Weather.Universal/Weather.Core/ViewModels/LocationViewModel.vb +++ b/Weather.Universal/Weather.Core/ViewModels/LocationViewModel.vb @@ -166,21 +166,12 @@ Namespace ViewModels #Region "Methods" Public Overrides Function InitializeAsync(Optional parameter As Object = Nothing) As Task - UpdateDataAsync() - - 'Dim timer As New DispatcherTimer - 'timer.Interval = TimeSpan.FromMinutes(1) - 'AddHandler timer.Tick, Sub(s, e) - ' UpdateDataAsync() - ' End Sub - 'timer.Start() - - 'Return Task.Delay(0) + Return Task.Delay(0) End Function Private Async Sub UpdateDataAsync() Try - LastChecked = DateTime.Now + LastChecked = DateTime.Now.ToString _cancelationTokenSource = New CancellationTokenSource Await UpdateCurrentObservationsAsync() diff --git a/Weather.Universal/Weather.Core/ViewModels/MainViewModel.vb b/Weather.Universal/Weather.Core/ViewModels/MainViewModel.vb index 4c916ef..8735d8c 100644 --- a/Weather.Universal/Weather.Core/ViewModels/MainViewModel.vb +++ b/Weather.Universal/Weather.Core/ViewModels/MainViewModel.vb @@ -1,6 +1,7 @@ Imports System.Threading Imports System.Windows.Input Imports Weather.Services +Imports Microsoft.Extensions.Logging Namespace ViewModels Public Class MainViewModel @@ -9,25 +10,31 @@ Namespace ViewModels Private ReadOnly _messageBus As IMessageBus Private ReadOnly _dialogService As IDialogService Private ReadOnly _navigationService As INavigationService + Private ReadOnly _locationService As ILocationService Private ReadOnly _settingsService As ISettingsService Private ReadOnly _weatherService As IWeatherService - Private ReadOnly _locationService As ILocationService - Private _locationTokenSource As CancellationTokenSource - #Region "Constructors" - Public Sub New() + 'Public Sub New() - End Sub + 'End Sub + + Public Sub New(messageBus As IMessageBus, + dialogService As IDialogService, + navigationService As INavigationService, + logger As ILogger(Of MainViewModel), + locationService As ILocationService, + settingsService As ISettingsService, + weatherService As IWeatherService) - Public Sub New(messageBus As IMessageBus, dialogService As IDialogService, navigationService As INavigationService, locationService As ILocationService, settingsService As ISettingsService, weatherService As IWeatherService) _messageBus = messageBus _dialogService = dialogService _navigationService = navigationService _locationService = locationService + _logger = logger _settingsService = settingsService _weatherService = weatherService @@ -67,12 +74,12 @@ Namespace ViewModels #Region "Progress" - Dim _Progress As Integer - Public Property Progress As Integer + Dim _Progress As Double + Public Property Progress As Double Get Return _Progress End Get - Set(value As Integer) + Private Set(value As Double) _Progress = value OnPropertyChanged("Progress") End Set @@ -124,7 +131,7 @@ Namespace ViewModels End Function Private Sub ExecuteAddLocation() - _navigationService.NavigateTo(Of AddWeatherSourceViewModel)() + _navigationService.NavigateTo(Of AddLocationViewModel)() End Sub #End Region @@ -141,41 +148,89 @@ Namespace ViewModels ''' This is the trunk logic for the view model. So, the try/catch will go here. ''' Public Overrides Async Function InitializeAsync(Optional parameter As Object = Nothing) As Task - If _IsIntilizing Then Return - _IsIntilizing = True - Try - Dim locations As IEnumerable(Of Models.Location) = Await _settingsService.GetSelectedLocationsAsync - ' We do no have a location. Detect the current location as accept that. - If locations Is Nothing Then - Dim tokenSource As New CancellationTokenSource - Dim currentGeoCoordinate As Models.GeoCoordinate = Await _settingsService.GetCurrentLocationAsync(New CancellationToken) - Dim location As Models.Location = Await _locationService.GetLocationByLatitudeLongitudeAsync(currentGeoCoordinate.Latitude, currentGeoCoordinate.Longitude, 3, tokenSource.Token) - - If location Is Nothing Then Return - locations = {location} - Await _settingsService.SetSelectedLocationsAsync(locations) - End If - - Dim currentObservationsTasks As New List(Of Task) - LocationViewModels.Clear() - For index = 0 To locations.Count - 1 - Dim viewModel As New LocationViewModel(locations(index), _settingsService, _weatherService) - LocationViewModels.Add(viewModel) - currentObservationsTasks.Add(viewModel.InitializeAsync(locations(index))) - Next - - Await Task.WhenAll(currentObservationsTasks) - Catch ex As Exception - ' TODO: We need to recover here! - - End Try - - _IsIntilizing = False + Dim locations As IEnumerable(Of Models.Location) = Nothing + + + Using _logger.BeginScope("InitializeAsync") + Try + If _IsIntilizing Then + _logger.LogInformation("ViewModel is Initializing. Bailing...") + Return + Else + _cancellationTokenSource = New CancellationTokenSource() + + Using _logger.BeginScope("Locations") + ' Get saved locations + locations = Await GetLocationsAsync() + If locations Is Nothing Then + ' Get the current location + Dim currentLocation As Models.Location = Await GetCurrentLocationAsync() + If currentLocation Is Nothing Then + ' Ask user to enter a location. + _logger.LogDebug("Navigating to AddWeatherSourceViewModel") + _navigationService.NavigateTo(Of AddLocationViewModel)() + Return + End If + + End If + End Using + _IsIntilizing = False + End If + Catch ex As Exception + _logger.LogError("There was an error: {0}", ex.Message) + End Try + End Using End Function + Private Async Function GetLocationsAsync() As Task(Of IEnumerable(Of Models.Location)) + Dim locations As IEnumerable(Of Models.Location) = Nothing + Using _logger.BeginScope("Saved Locations") + Try + _logger.LogInformation("Getting locations...") + locations = Await _settingsService.GetLocationsAsync(_cancellationTokenSource.Token) + If locations Is Nothing Then _logger.LogInformation("There are no saved locations") + Catch ex As Exception + _logger.LogError("There was an error: {0}", ex.Message) + End Try + End Using + Return locations + End Function + Private Async Function GetCurrentLocationAsync() As Task(Of Models.Location) + Dim location As Models.Location = Nothing + + Using _logger.BeginScope("Current Location") + Try + _logger.LogInformation("Getting Current Location...") + Dim currentGeoCoordinate As Models.GeoCoordinate = Await _settingsService.GetCurrentLocationAsync(_cancellationTokenSource.Token) + If _cancellationTokenSource.Token.IsCancellationRequested Then Return Nothing + If currentGeoCoordinate Is Nothing Then + _logger.LogWarning("Application cannot retrieve current geographical coordinate.") + Return Nothing + Else + _logger.LogDebug("Retrieved geographical coordinate: {0}:{1}", currentGeoCoordinate.Latitude, currentGeoCoordinate.Longitude) + End If + + _logger.LogInformation("Getting location by latitude, longitude", currentGeoCoordinate.Latitude, currentGeoCoordinate.Longitude) + + location = Await _locationService.GetLocationByLatitudeLongitudeAsync(currentGeoCoordinate.Latitude, currentGeoCoordinate.Longitude, 3, _cancellationTokenSource.Token) + If _cancellationTokenSource.IsCancellationRequested Then Return Nothing + If location Is Nothing Then + _logger.LogWarning("Application cannot retrieve current location.") + Return Nothing + Else + _logger.LogDebug("Retrieved location: {0}", location.Address.DisplayString) + End If + + Catch ex As Exception + _logger.LogError("There was an error: {0}", ex.Message) + End Try + End Using + + Return location + End Function #End Region diff --git a/Weather.Universal/Weather.Core/ViewModels/WeatherSourcesViewModel.vb b/Weather.Universal/Weather.Core/ViewModels/WeatherSourcesViewModel.vb index e5a75c0..40afba3 100644 --- a/Weather.Universal/Weather.Core/ViewModels/WeatherSourcesViewModel.vb +++ b/Weather.Universal/Weather.Core/ViewModels/WeatherSourcesViewModel.vb @@ -67,7 +67,7 @@ Namespace ViewModels End Function Private Sub ExecuteAddWeatherSource() - _navigationService.NavigateTo(Of AddWeatherSourceViewModel)() + _navigationService.NavigateTo(Of AddLocationViewModel)() End Sub #End Region diff --git a/Weather.Universal/Weather.Core/ViewModels/_EditViewModelBase.vb b/Weather.Universal/Weather.Core/ViewModels/_EditViewModelBase.vb new file mode 100644 index 0000000..c543de1 --- /dev/null +++ b/Weather.Universal/Weather.Core/ViewModels/_EditViewModelBase.vb @@ -0,0 +1,113 @@ +Namespace ViewModels + + Public Class EditViewModelBase + Inherits ViewModelBase + Implements INotifyDataErrorInfo + + + Private Event ErrorsChanged As EventHandler(Of DataErrorsChangedEventArgs) Implements INotifyDataErrorInfo.ErrorsChanged + + + ' A dictionary to hold all the errors. + Private ReadOnly _errors As New Dictionary(Of String, ObservableCollection(Of String)) + ''' + ''' Returns a containing all the error messages. + ''' + ''' A containing all the error messages + Public ReadOnly Property Errors As Dictionary(Of String, ObservableCollection(Of String)) + + Get + Return _errors + End Get + End Property + + + Private ReadOnly _allErrors As New ObservableCollection(Of String) + ''' + ''' A collection of all the errors as strings. + ''' + ''' An containing all the error messages. + Public ReadOnly Property AllErrors As ObservableCollection(Of String) + Get + Return _allErrors + End Get + End Property + + ''' + ''' Adds an error message to the underlying collection using the given property name. + ''' + ''' The error message for the property. + ''' The property that is invalid. + Protected Sub AddError(errorMessage As String, Optional propertyName As String = Nothing) + If Not _errors.ContainsKey(propertyName) Then + _errors.Add(propertyName, New ObservableCollection(Of String)) + End If + + If Not _errors(propertyName).Contains(errorMessage) Then _errors(propertyName).Add(errorMessage) + If Not _allErrors.Contains(errorMessage) Then _allErrors.Add(errorMessage) + RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName)) + End Sub + + ''' + ''' Removed an error message to the underlying collection using the given property name, if it exists. + ''' + ''' The error message for the property. + ''' The property that the error message relates to. + Protected Sub RemoveError(errorMessage As String, Optional propertyName As String = Nothing) + If _errors.ContainsKey(propertyName) AndAlso _errors(propertyName).Contains(errorMessage) Then + _errors(propertyName).Remove(errorMessage) + If _errors(propertyName).Count = 0 Then _errors.Remove(propertyName) + End If + + If _allErrors.Contains(errorMessage) Then _allErrors.Remove(errorMessage) + + RaiseEvent ErrorsChanged(Me, New DataErrorsChangedEventArgs(propertyName)) + End Sub + + ''' + ''' Implements . Returns all the errors that are related to a given property. + ''' + ''' The property name. + ''' An containing errors; Otherwise Nothing. + Public Function GetErrors(propertyName As String) As IEnumerable Implements INotifyDataErrorInfo.GetErrors + If String.IsNullOrEmpty(propertyName) Then Return Nothing + If Not _errors.ContainsKey(propertyName) Then Return Nothing + Return _errors(propertyName) + End Function + + ''' + ''' Occurs when the error collections have changed. + ''' + ''' The sender of the event. + ''' The containing the changed event. + Private Sub EditViewModelBase_ErrorsChanged(sender As Object, e As DataErrorsChangedEventArgs) Handles Me.ErrorsChanged + OnPropertyChanged("AllErrors") + OnPropertyChanged("Errors") + OnPropertyChanged("HasErrors") + OnPropertyChanged("IsValid") + End Sub + + ''' + ''' Implements . Returns whether or not the view model has errors. + ''' + ''' True if there are errors; Otherwise False. + Private ReadOnly Property HasErrors As Boolean Implements INotifyDataErrorInfo.HasErrors + Get + Return Errors.Count > 0 + End Get + End Property + + ''' + ''' Used to bind to a property or a command to become available when the view model contains not errors. + ''' + ''' True if valid; Otherwise False. + Public ReadOnly Property IsValid As Boolean + Get + Return _errors.Count = 0 + End Get + End Property + + End Class + +End Namespace + diff --git a/Weather.Universal/Weather.Core/ViewModels/_ViewModelBase.vb b/Weather.Universal/Weather.Core/ViewModels/_ViewModelBase.vb index e5f2a20..ed63b88 100644 --- a/Weather.Universal/Weather.Core/ViewModels/_ViewModelBase.vb +++ b/Weather.Universal/Weather.Core/ViewModels/_ViewModelBase.vb @@ -1,14 +1,18 @@ Imports System.Linq Imports System.Reflection Imports System.Threading +Imports Microsoft.Extensions.Logging Namespace ViewModels Public Class ViewModelBase Inherits ObservableObject + Implements IDisposable Private Const APPMANIFESTNAME As String = "WMAppManifest.xml" Protected _IsIntilizing As Boolean + Protected _cancellationTokenSource As CancellationTokenSource + Protected _logger As ILogger Public Sub New() @@ -20,6 +24,44 @@ Namespace ViewModels _IsIntilizing = False End Function + Protected Function CheckIsCanceled() As Boolean + If _cancellationTokenSource.IsCancellationRequested AndAlso _logger IsNot Nothing Then _logger.LogInformation("Transaction Canceled") + + Return _cancellationTokenSource.IsCancellationRequested + End Function + +#Region "IDisposable Support" + Private disposedValue As Boolean ' To detect redundant calls + + ' IDisposable + Protected Overridable Sub Dispose(disposing As Boolean) + If Not disposedValue Then + If disposing Then + If _cancellationTokenSource IsNot Nothing Then _cancellationTokenSource.Cancel() + End If + + ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below. + ' TODO: set large fields to null. + End If + disposedValue = True + End Sub + + ' TODO: override Finalize() only if Dispose(disposing As Boolean) above has code to free unmanaged resources. + 'Protected Overrides Sub Finalize() + ' ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above. + ' Dispose(False) + ' MyBase.Finalize() + 'End Sub + + ' This code added by Visual Basic to correctly implement the disposable pattern. + Public Sub Dispose() Implements IDisposable.Dispose + ' Do not change this code. Put cleanup code in Dispose(disposing As Boolean) above. + Dispose(True) + ' TODO: uncomment the following line if Finalize() is overridden above. + ' GC.SuppressFinalize(Me) + End Sub +#End Region + #Region "Properties" Private _applicationTitle As String diff --git a/Weather.Universal/Weather.Core/Weather.Core.vbproj b/Weather.Universal/Weather.Core/Weather.Core.vbproj index c26f326..8bf5682 100644 --- a/Weather.Universal/Weather.Core/Weather.Core.vbproj +++ b/Weather.Universal/Weather.Core/Weather.Core.vbproj @@ -23,8 +23,8 @@ bin\Debug Weather.Core.xml - 40057,42016,41999,42020,42021,42022 - 42017,42018,42019,42032,42036 + 40057 + 41999,42016,42017,42018,42019,42020,42021,42022,42032,42036 pdbonly @@ -35,8 +35,8 @@ true bin\Release Weather.Core.xml - 40057,42016,41999,42020,42021,42022 - 42017,42018,42019,42032,42036 + 40057 + 41999,42016,42017,42018,42019,42020,42021,42022,42032,42036 On @@ -45,7 +45,7 @@ Binary - Off + On On @@ -86,6 +86,7 @@ + @@ -103,13 +104,14 @@ - + - + - + + @@ -118,6 +120,12 @@ + + + ..\packages\Microsoft.Extensions.Logging.Abstractions.1.1.2\lib\netstandard1.1\Microsoft.Extensions.Logging.Abstractions.dll + False + + MSBuild:Compile @@ -136,6 +144,9 @@ Designer + + + diff --git a/Weather.Universal/Weather.WPF/Application.xaml b/Weather.Universal/Weather.WPF/Application.xaml index 27360bd..0c7c8bd 100644 --- a/Weather.Universal/Weather.WPF/Application.xaml +++ b/Weather.Universal/Weather.WPF/Application.xaml @@ -4,21 +4,29 @@ xmlns:local="clr-namespace:Weather.WPF" xmlns:clr="clr-namespace:System;assembly=mscorlib"> + + + + + + diff --git a/Weather.Universal/Weather.WPF/Application.xaml.vb b/Weather.Universal/Weather.WPF/Application.xaml.vb index 69a07d0..75cbea2 100644 --- a/Weather.Universal/Weather.WPF/Application.xaml.vb +++ b/Weather.Universal/Weather.WPF/Application.xaml.vb @@ -1,8 +1,7 @@ -Imports Windows.UI.Xaml -Imports Windows.UI.Xaml.Media.Animation -Imports Microsoft.Extensions.DependencyInjection +Imports Microsoft.Extensions.DependencyInjection Imports Weather.ViewModels Imports Weather.Services +Imports Microsoft.Extensions.Logging Class Application @@ -22,15 +21,6 @@ Class Application End Property - Protected Overrides Sub OnStartup(e As StartupEventArgs) - _mainWindow = ApplicationMainWindow - ApplicationMainWindow.Show() - - Dim navigationService = _container.GetRequiredService(Of INavigationService)() - navigationService.NavigateTo(Of MainViewModel)() - End Sub - - ''' ''' Initializes the singleton application object. This is the first line of authored code ''' executed, and as such is the logical equivalent of main() or WinMain(). @@ -43,6 +33,7 @@ Class Application ' Services services.AddSingleton(Of INavigationService, Services.NavigationService) services.AddSingleton(Of IMessageBus, MessageBus) + services.AddLogging() services.AddTransient(Of IDialogService, Services.DialogService) services.AddTransient(Of ISettingsService, Services.SettingsService) @@ -51,10 +42,26 @@ Class Application ' ViewModels services.AddTransient(Of MainViewModel) + services.AddTransient(Of AddLocationViewModel) _container = services.BuildServiceProvider + End Sub + + + Protected Overrides Sub OnStartup(e As StartupEventArgs) + _mainWindow = ApplicationMainWindow + ApplicationMainWindow.Show() + + Dim loggerFactory As ILoggerFactory = _container.GetRequiredService(Of ILoggerFactory) + loggerFactory.AddDebug(LogLevel.Debug) + + Dim navigationService = _container.GetRequiredService(Of INavigationService)() + navigationService.NavigateTo(Of MainViewModel)() End Sub + + + End Class diff --git a/Weather.Universal/Weather.WPF/DataTemplates.xaml b/Weather.Universal/Weather.WPF/DataTemplates.xaml new file mode 100644 index 0000000..d798cc6 --- /dev/null +++ b/Weather.Universal/Weather.WPF/DataTemplates.xaml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Weather.Universal/Weather.WPF/SampleData/AddWeatherSourceViewModelSampleData.xaml b/Weather.Universal/Weather.WPF/SampleData/AddWeatherSourceViewModelSampleData.xaml index 2179413..2aaeca2 100644 --- a/Weather.Universal/Weather.WPF/SampleData/AddWeatherSourceViewModelSampleData.xaml +++ b/Weather.Universal/Weather.WPF/SampleData/AddWeatherSourceViewModelSampleData.xaml @@ -1,4 +1,4 @@ - diff --git a/Weather.Universal/Weather.WPF/Services/LocationService.vb b/Weather.Universal/Weather.WPF/Services/LocationService.vb index 2d4b2dc..94938e9 100644 --- a/Weather.Universal/Weather.WPF/Services/LocationService.vb +++ b/Weather.Universal/Weather.WPF/Services/LocationService.vb @@ -7,7 +7,11 @@ Namespace Services Public Class LocationService Implements ILocationService - Public Function GetLocationByLatitudeLongitudeAsync(latitude As Decimal, longitude As Decimal, numberOfStations As Integer, token As CancellationToken) As Task(Of Location) Implements ILocationService.GetLocationByLatitudeLongitudeAsync + Public Function GetLocationByCityAndStateAsync(city As String, state As String, numberOfStations As Integer, token As CancellationToken) As Task(Of Location) Implements ILocationService.GetLocationByCityAndStateAsync + Throw New NotImplementedException() + End Function + + Public Function GetLocationByLatitudeLongitudeAsync(latitude As Double, longitude As Double, numberOfStations As Integer, token As CancellationToken) As Task(Of Location) Implements ILocationService.GetLocationByLatitudeLongitudeAsync Throw New NotImplementedException() End Function diff --git a/Weather.Universal/Weather.WPF/Services/NavigationService.vb b/Weather.Universal/Weather.WPF/Services/NavigationService.vb index 2b9cedb..4e1340b 100644 --- a/Weather.Universal/Weather.WPF/Services/NavigationService.vb +++ b/Weather.Universal/Weather.WPF/Services/NavigationService.vb @@ -74,7 +74,7 @@ Namespace Services End Sub Public Async Sub NavigateTo(Of TViewModel As ViewModelBase)(Optional addToHistory As Boolean = True) Implements INavigationService.NavigateTo - Dim viewModel As ViewModelBase = Application.Container.GetService(GetType(TViewModel)) + Dim viewModel As ViewModelBase = Application.Container.GetRequiredService(GetType(TViewModel)) Application.ApplicationMainWindow.MainContent.Content = viewModel If addToHistory Then _history.Add(GetType(TViewModel)) diff --git a/Weather.Universal/Weather.WPF/Services/SettingsService.vb b/Weather.Universal/Weather.WPF/Services/SettingsService.vb index 5e7fb35..dd72297 100644 --- a/Weather.Universal/Weather.WPF/Services/SettingsService.vb +++ b/Weather.Universal/Weather.WPF/Services/SettingsService.vb @@ -6,21 +6,11 @@ Namespace Services Public Class SettingsService Implements ISettingsService - Public Async Function GetCurrentLocationAsync(token As CancellationToken) As Task(Of Models.GeoCoordinate) Implements ISettingsService.GetCurrentLocationAsync - Throw New NotImplementedException - 'Dim geolocation As New Geolocator - 'Dim result As Geoposition = Await geolocation.GetGeopositionAsync.AsTask - - 'If result Is Nothing Then - ' Return Nothing - 'Else - ' Return New Models.GeoCoordinate With { - ' .Latitude = CType(result.Coordinate.Latitude, Decimal), - ' .Longitude = CType(result.Coordinate.Longitude, Decimal)} - 'End If + Public Function GetCurrentLocationAsync(token As CancellationToken) As Task(Of Models.GeoCoordinate) Implements ISettingsService.GetCurrentLocationAsync + Return Task.Delay(0) End Function - Public Async Function GetSelectedLocationsAsync() As Task(Of IEnumerable(Of Location)) Implements ISettingsService.GetSelectedLocationsAsync + Public Async Function GetLocationsAsync(token As CancellationToken) As Task(Of IEnumerable(Of Location)) Implements ISettingsService.GetLocationsAsync Await Task.Delay(0) Return Nothing End Function diff --git a/Weather.Universal/Weather.WPF/Views/AddLocationView.xaml b/Weather.Universal/Weather.WPF/Views/AddLocationView.xaml new file mode 100644 index 0000000..e683a21 --- /dev/null +++ b/Weather.Universal/Weather.WPF/Views/AddLocationView.xaml @@ -0,0 +1,23 @@ + + + + + + + + Search by zip code or City, State + + diff --git a/Weather.Universal/Weather.WPF/Views/AddLocationView.xaml.vb b/Weather.Universal/Weather.WPF/Views/AddLocationView.xaml.vb new file mode 100644 index 0000000..d51c52a --- /dev/null +++ b/Weather.Universal/Weather.WPF/Views/AddLocationView.xaml.vb @@ -0,0 +1,3 @@ +Public Class AddLocationView + +End Class diff --git a/Weather.Universal/Weather.WPF/Views/CurrentObservationsView.xaml b/Weather.Universal/Weather.WPF/Views/CurrentObservationsView.xaml index 5e88e8e..a732628 100644 --- a/Weather.Universal/Weather.WPF/Views/CurrentObservationsView.xaml +++ b/Weather.Universal/Weather.WPF/Views/CurrentObservationsView.xaml @@ -24,7 +24,7 @@ - + diff --git a/Weather.Universal/Weather.WPF/Weather.WPF.vbproj b/Weather.Universal/Weather.WPF/Weather.WPF.vbproj index 8422d26..63ba0bc 100644 --- a/Weather.Universal/Weather.WPF/Weather.WPF.vbproj +++ b/Weather.Universal/Weather.WPF/Weather.WPF.vbproj @@ -13,6 +13,8 @@ Custom true + + AnyCPU @@ -50,14 +52,6 @@ On - - ..\packages\ManagedEsent.1.9.4\lib\net40\Esent.Interop.dll - True - - - ..\packages\Microsoft.Database.Isam.1.9.4\lib\net40\Esent.Isam.dll - True - ..\packages\Microsoft.Extensions.DependencyInjection.1.1.1\lib\netstandard1.1\Microsoft.Extensions.DependencyInjection.dll True @@ -66,94 +60,37 @@ ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.1.1.1\lib\netstandard1.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll True - - ..\packages\Microsoft.Win32.Primitives.4.3.0\lib\net46\Microsoft.Win32.Primitives.dll - True - - - ..\packages\System.AppContext.4.3.0\lib\net46\System.AppContext.dll - True - - - - ..\packages\System.Console.4.3.0\lib\net46\System.Console.dll - True - - - - ..\packages\System.Diagnostics.DiagnosticSource.4.3.0\lib\net46\System.Diagnostics.DiagnosticSource.dll - True - - - ..\packages\System.Globalization.Calendars.4.3.0\lib\net46\System.Globalization.Calendars.dll - True - - - ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll - True - - - - ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll - True - - - ..\packages\System.IO.FileSystem.4.3.0\lib\net46\System.IO.FileSystem.dll - True - - - ..\packages\System.IO.FileSystem.Primitives.4.3.0\lib\net46\System.IO.FileSystem.Primitives.dll + + ..\packages\Microsoft.Extensions.Logging.1.1.2\lib\netstandard1.1\Microsoft.Extensions.Logging.dll True - - ..\packages\System.Net.Http.4.3.0\lib\net46\System.Net.Http.dll + + ..\packages\Microsoft.Extensions.Logging.Abstractions.1.1.2\lib\netstandard1.1\Microsoft.Extensions.Logging.Abstractions.dll True - - ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll + + ..\packages\Microsoft.Extensions.Logging.Debug.1.1.2\lib\net451\Microsoft.Extensions.Logging.Debug.dll True + + + + + + + + + ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll True - - False - C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5.1\System.Runtime.WindowsRuntime.dll - - - ..\packages\System.Security.Cryptography.Algorithms.4.3.0\lib\net461\System.Security.Cryptography.Algorithms.dll - True - - - ..\packages\System.Security.Cryptography.Encoding.4.3.0\lib\net46\System.Security.Cryptography.Encoding.dll - True - - - ..\packages\System.Security.Cryptography.Primitives.4.3.0\lib\net46\System.Security.Cryptography.Primitives.dll - True - - - ..\packages\System.Security.Cryptography.X509Certificates.4.3.0\lib\net461\System.Security.Cryptography.X509Certificates.dll - True - + + + - - - - - - 4.0 - - - ..\packages\System.Xml.ReaderWriter.4.3.0\lib\net46\System.Xml.ReaderWriter.dll - True - - - - @@ -170,15 +107,19 @@ + + AddLocationView.xaml + CurrentObservationsView.xaml MainView.xaml - - UserControl1.xaml - + + Designer + MSBuild:Compile + MSBuild:Compile Designer @@ -199,6 +140,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + MSBuild:Compile Designer @@ -207,10 +152,6 @@ Designer MSBuild:Compile - - Designer - MSBuild:Compile - diff --git a/Weather.Universal/Weather.WPF/packages.config b/Weather.Universal/Weather.WPF/packages.config index 14de73c..499754d 100644 --- a/Weather.Universal/Weather.WPF/packages.config +++ b/Weather.Universal/Weather.WPF/packages.config @@ -1,54 +1,41 @@  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file