diff --git a/Filtration.Tests/Filtration.Tests.csproj b/Filtration.Tests/Filtration.Tests.csproj index d26871f..5a248a6 100644 --- a/Filtration.Tests/Filtration.Tests.csproj +++ b/Filtration.Tests/Filtration.Tests.csproj @@ -48,8 +48,10 @@ + + diff --git a/Filtration.Tests/Services/TestHTTPService.cs b/Filtration.Tests/Services/TestHTTPService.cs new file mode 100644 index 0000000..54b7665 --- /dev/null +++ b/Filtration.Tests/Services/TestHTTPService.cs @@ -0,0 +1,23 @@ +using Filtration.Services; +using NUnit.Framework; + +namespace Filtration.Tests.Services +{ + [Ignore("Integration Test - Makes real HTTP call")] + [TestFixture] + public class TestHTTPService + { + [Test] + public async void GetContent_FetchesDataFromUrl() + { + // Arrange + var service = new HTTPService(); + + // Act + var result = await service.GetContentAsync("http://ben-wallis.github.io/Filtration/filtration_version.xml"); + + // Assert + Assert.IsNotNull(result); + } + } +} diff --git a/Filtration.Tests/Services/TestUpdateService.cs b/Filtration.Tests/Services/TestUpdateService.cs new file mode 100644 index 0000000..343c3ee --- /dev/null +++ b/Filtration.Tests/Services/TestUpdateService.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Filtration.Models; +using Filtration.Services; +using Moq; +using NUnit.Framework; + +namespace Filtration.Tests.Services +{ + [TestFixture] + public class TestUpdateService + { + [Test] + public void DeserializeUpdateData_ReturnsCorrectData() + { + // Arrange + var testInputData = @" + 0.2 + 2015-07-01 + http://www.google.com + * Release notes line 1 +* Release notes line 2 +* More really great release notes! + "; + + var expectedResult = new UpdateData + { + CurrentVersion = 0.2m, + DownloadUrl = "http://www.google.com", + ReleaseDate = new DateTime(2015, 7, 1), + ReleaseNotes = @"* Release notes line 1 +* Release notes line 2 +* More really great release notes!" + }; + + var mockHTTPService = new Mock(); + var service = new UpdateCheckService(mockHTTPService.Object); + + // Act + var result = service.DeserializeUpdateData(testInputData); + + // Assert + Assert.AreEqual(expectedResult.CurrentVersion, result.CurrentVersion); + Assert.AreEqual(expectedResult.DownloadUrl, result.DownloadUrl); + Assert.AreEqual(expectedResult.ReleaseDate, result.ReleaseDate); + Assert.AreEqual(expectedResult.ReleaseNotes, result.ReleaseNotes); + } + + } +} diff --git a/Filtration/App.config b/Filtration/App.config index eee4e61..9cd140a 100644 --- a/Filtration/App.config +++ b/Filtration/App.config @@ -16,6 +16,12 @@ True + + 0 + + + False + diff --git a/Filtration/Filtration.csproj b/Filtration/Filtration.csproj index 5421666..5c4307c 100644 --- a/Filtration/Filtration.csproj +++ b/Filtration/Filtration.csproj @@ -139,10 +139,13 @@ + + + @@ -172,6 +175,7 @@ + AvalonDockWorkspaceView.xaml @@ -223,6 +227,9 @@ StartPageView.xaml + + UpdateAvailableView.xaml + @@ -330,6 +337,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + @@ -397,7 +408,6 @@ - Always diff --git a/Filtration/Models/UpdateData.cs b/Filtration/Models/UpdateData.cs new file mode 100644 index 0000000..b7a556a --- /dev/null +++ b/Filtration/Models/UpdateData.cs @@ -0,0 +1,25 @@ +using System; +using System.Text.RegularExpressions; + +namespace Filtration.Models +{ + [Serializable] + public class UpdateData + { + private string _releaseNotes; + + public string DownloadUrl { get; set; } + public decimal CurrentVersion { get; set; } + public DateTime ReleaseDate { get; set; } + + public string ReleaseNotes + { + get { return _releaseNotes; } + set + { + var r = new Regex("(? True + + 0 + + + False + \ No newline at end of file diff --git a/Filtration/Resources/Images/doge.jpg b/Filtration/Resources/Images/doge.jpg deleted file mode 100644 index bfb772c..0000000 Binary files a/Filtration/Resources/Images/doge.jpg and /dev/null differ diff --git a/Filtration/Services/HTTPService.cs b/Filtration/Services/HTTPService.cs new file mode 100644 index 0000000..a9f728e --- /dev/null +++ b/Filtration/Services/HTTPService.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Net; +using System.Threading.Tasks; + +namespace Filtration.Services +{ + internal interface IHTTPService + { + Task GetContentAsync(string url); + } + + internal class HTTPService : IHTTPService + { + public async Task GetContentAsync(string url) + { + var urlUri = new Uri(url); + + var request = WebRequest.Create(urlUri); + + var response = await request.GetResponseAsync(); + using (var s = response.GetResponseStream()) + { + using (var sr = new StreamReader(s)) + { + return sr.ReadToEnd(); + } + } + } + } +} diff --git a/Filtration/Services/UpdateCheckService.cs b/Filtration/Services/UpdateCheckService.cs new file mode 100644 index 0000000..5a01c83 --- /dev/null +++ b/Filtration/Services/UpdateCheckService.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Serialization; +using Filtration.Models; + +namespace Filtration.Services +{ + internal interface IUpdateCheckService + { + Task GetUpdateData(); + } + + internal class UpdateCheckService : IUpdateCheckService + { + private readonly IHTTPService _httpService; + private const string UpdateDataUrl = "http://ben-wallis.github.io/Filtration/filtration_version.xml"; + + public UpdateCheckService(IHTTPService httpService) + { + _httpService = httpService; + } + + public async Task GetUpdateData() + { + var updateXml = await _httpService.GetContentAsync(UpdateDataUrl); + return (DeserializeUpdateData(updateXml)); + } + + public UpdateData DeserializeUpdateData(string updateDataString) + { + var serializer = new XmlSerializer(typeof(UpdateData)); + object result; + + using (TextReader reader = new StringReader(updateDataString)) + { + result = serializer.Deserialize(reader); + } + + return result as UpdateData; + } + + } +} diff --git a/Filtration/ViewModels/MainWindowViewModel.cs b/Filtration/ViewModels/MainWindowViewModel.cs index e0a29df..720fddd 100644 --- a/Filtration/ViewModels/MainWindowViewModel.cs +++ b/Filtration/ViewModels/MainWindowViewModel.cs @@ -2,13 +2,17 @@ using System.Diagnostics; using System.IO; using System.Reflection; +using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Media; using System.Windows.Media.Imaging; using Filtration.Common.ViewModels; using Filtration.Interface; +using Filtration.Models; using Filtration.ObjectModel.ThemeEditor; +using Filtration.Properties; using Filtration.Repositories; +using Filtration.Services; using Filtration.ThemeEditor.Providers; using Filtration.ThemeEditor.Services; using Filtration.ThemeEditor.ViewModels; @@ -36,6 +40,8 @@ namespace Filtration.ViewModels private readonly ISettingsPageViewModel _settingsPageViewModel; private readonly IThemeProvider _themeProvider; private readonly IThemeService _themeService; + private readonly IUpdateCheckService _updateCheckService; + private readonly IUpdateAvailableViewModel _updateAvailableViewModel; public MainWindowViewModel(IItemFilterScriptRepository itemFilterScriptRepository, IItemFilterScriptTranslator itemFilterScriptTranslator, @@ -43,7 +49,9 @@ namespace Filtration.ViewModels IAvalonDockWorkspaceViewModel avalonDockWorkspaceViewModel, ISettingsPageViewModel settingsPageViewModel, IThemeProvider themeProvider, - IThemeService themeService) + IThemeService themeService, + IUpdateCheckService updateCheckService, + IUpdateAvailableViewModel updateAvailableViewModel) { _itemFilterScriptRepository = itemFilterScriptRepository; _itemFilterScriptTranslator = itemFilterScriptTranslator; @@ -52,6 +60,8 @@ namespace Filtration.ViewModels _settingsPageViewModel = settingsPageViewModel; _themeProvider = themeProvider; _themeService = themeService; + _updateCheckService = updateCheckService; + _updateAvailableViewModel = updateAvailableViewModel; NewScriptCommand = new RelayCommand(OnNewScriptCommand); CopyScriptCommand = new RelayCommand(OnCopyScriptCommand, () => ActiveDocumentIsScript); @@ -128,6 +138,7 @@ namespace Filtration.ViewModels } } }); + CheckForUpdates(); } public RelayCommand OpenScriptCommand { get; private set; } @@ -162,6 +173,37 @@ namespace Filtration.ViewModels public RelayCommand ClearFiltersCommand { get; private set; } + public async void CheckForUpdates() + { + var assembly = Assembly.GetExecutingAssembly(); + var assemblyVersion = FileVersionInfo.GetVersionInfo(assembly.Location); + var currentVersion = Convert.ToDecimal(assemblyVersion.FileMajorPart + "." + assemblyVersion.FileMinorPart); + + var result = await _updateCheckService.GetUpdateData(); + + try + { + if (result.CurrentVersion > currentVersion) + { + if (Settings.Default.SuppressUpdates == false || + Settings.Default.SuppressUpdatesUpToVersion < result.CurrentVersion) + { + Settings.Default.SuppressUpdates = false; + Settings.Default.Save(); + var updateAvailableView = new UpdateAvailableView {DataContext = _updateAvailableViewModel}; + _updateAvailableViewModel.Initialise(result, currentVersion); + _updateAvailableViewModel.OnRequestClose += (s, e) => updateAvailableView.Close(); + updateAvailableView.ShowDialog(); + } + } + } + catch (Exception) + { + // We don't care if the update check fails, because it could fail for multiple reasons + // including the user blocking Filtration in their firewall. + } + } + public ImageSource Icon { get; private set; } public IAvalonDockWorkspaceViewModel AvalonDockWorkspaceViewModel diff --git a/Filtration/ViewModels/SettingsPageViewModel.cs b/Filtration/ViewModels/SettingsPageViewModel.cs index 2526017..ac99b2a 100644 --- a/Filtration/ViewModels/SettingsPageViewModel.cs +++ b/Filtration/ViewModels/SettingsPageViewModel.cs @@ -22,11 +22,13 @@ namespace Filtration.ViewModels DefaultFilterDirectory = Settings.Default.DefaultFilterDirectory; ExtraLineBetweenBlocks = Settings.Default.ExtraLineBetweenBlocks; + SuppressUpdateNotifications = Settings.Default.SuppressUpdates; } public RelayCommand SaveCommand { get; private set; } public string DefaultFilterDirectory { get; set; } public bool ExtraLineBetweenBlocks { get; set; } + public bool SuppressUpdateNotifications { get; set; } private void OnSaveCommand() { @@ -35,6 +37,7 @@ namespace Filtration.ViewModels _itemFilterPersistenceService.SetItemFilterScriptDirectory(DefaultFilterDirectory); Settings.Default.ExtraLineBetweenBlocks = ExtraLineBetweenBlocks; + Settings.Default.SuppressUpdates = SuppressUpdateNotifications; } catch (DirectoryNotFoundException) { diff --git a/Filtration/ViewModels/UpdateAvailableViewModel.cs b/Filtration/ViewModels/UpdateAvailableViewModel.cs new file mode 100644 index 0000000..5cf6ed0 --- /dev/null +++ b/Filtration/ViewModels/UpdateAvailableViewModel.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Filtration.Models; +using Filtration.Properties; +using GalaSoft.MvvmLight.CommandWpf; + +namespace Filtration.ViewModels +{ + internal interface IUpdateAvailableViewModel + { + event EventHandler OnRequestClose; + void Initialise(UpdateData updateData, decimal currentVersion); + decimal CurrentVersion { get; } + decimal NewVersion { get; } + string ReleaseNotes { get; } + DateTime ReleaseDate { get; } + } + + internal class UpdateAvailableViewModel : IUpdateAvailableViewModel + { + private UpdateData _updateData; + private decimal _currentVersion; + + public UpdateAvailableViewModel() + { + DownloadCommand = new RelayCommand(OnDownloadCommand); + AskLaterCommand = new RelayCommand(OnAskLaterCommand); + NeverAskAgainCommand = new RelayCommand(OnNeverAskAgainCommand); + } + + public event EventHandler OnRequestClose; + + public RelayCommand NeverAskAgainCommand { get; private set; } + public RelayCommand AskLaterCommand { get; private set; } + public RelayCommand DownloadCommand { get; private set; } + + public void Initialise(UpdateData updateData, decimal currentVersion) + { + _currentVersion = currentVersion; + _updateData = updateData; + } + + public decimal CurrentVersion + { + get { return _currentVersion; } + } + + public decimal NewVersion + { + get { return _updateData.CurrentVersion; } + } + + public string ReleaseNotes + { + get { return _updateData.ReleaseNotes; } + } + + public DateTime ReleaseDate + { + get { return _updateData.ReleaseDate; } + } + + private void OnDownloadCommand() + { + Process.Start(_updateData.DownloadUrl); + } + + private void OnNeverAskAgainCommand() + { + Settings.Default.SuppressUpdates = true; + Settings.Default.SuppressUpdatesUpToVersion = _updateData.CurrentVersion; + Settings.Default.Save(); + CloseWindow(); + } + + private void OnAskLaterCommand() + { + CloseWindow(); + } + + private void CloseWindow() + { + if (OnRequestClose != null) + { + OnRequestClose(this, new EventArgs()); + } + } + } + +} + diff --git a/Filtration/Views/MainWindow.xaml b/Filtration/Views/MainWindow.xaml index 3e44721..ff8670e 100644 --- a/Filtration/Views/MainWindow.xaml +++ b/Filtration/Views/MainWindow.xaml @@ -28,8 +28,7 @@ - One day there will be something here! Maybe recent documents or something? For now here's a picture of Doge: - + Recent Documents will go here in a future release. diff --git a/Filtration/Views/SettingsPageView.xaml b/Filtration/Views/SettingsPageView.xaml index 20a7c6e..a462640 100644 --- a/Filtration/Views/SettingsPageView.xaml +++ b/Filtration/Views/SettingsPageView.xaml @@ -25,11 +25,15 @@ + + Default Filter Directory: - Add blank line between blocks when saving - + Add blank line between blocks when saving + + Suppress update notifications for current version + diff --git a/Filtration/Views/UpdateAvailableView.xaml b/Filtration/Views/UpdateAvailableView.xaml new file mode 100644 index 0000000..a147587 --- /dev/null +++ b/Filtration/Views/UpdateAvailableView.xaml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Filtration/Views/UpdateAvailableView.xaml.cs b/Filtration/Views/UpdateAvailableView.xaml.cs new file mode 100644 index 0000000..23004c7 --- /dev/null +++ b/Filtration/Views/UpdateAvailableView.xaml.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Shapes; + +namespace Filtration.Views +{ + /// + /// Interaction logic for UpdateAvailableView.xaml + /// + public partial class UpdateAvailableView : Window + { + public UpdateAvailableView() + { + InitializeComponent(); + } + } +} diff --git a/Filtration/WindsorInstallers/ServicesInstaller.cs b/Filtration/WindsorInstallers/ServicesInstaller.cs index 51058e7..107ffd4 100644 --- a/Filtration/WindsorInstallers/ServicesInstaller.cs +++ b/Filtration/WindsorInstallers/ServicesInstaller.cs @@ -24,6 +24,16 @@ namespace Filtration.WindsorInstallers Component.For() .ImplementedBy() .LifeStyle.Singleton); + + container.Register( + Component.For() + .ImplementedBy() + .LifeStyle.Singleton); + + container.Register( + Component.For() + .ImplementedBy() + .LifeStyle.Singleton); } } } diff --git a/Filtration/WindsorInstallers/ViewModelsInstaller.cs b/Filtration/WindsorInstallers/ViewModelsInstaller.cs index af96427..d002ac1 100644 --- a/Filtration/WindsorInstallers/ViewModelsInstaller.cs +++ b/Filtration/WindsorInstallers/ViewModelsInstaller.cs @@ -60,7 +60,12 @@ namespace Filtration.WindsorInstallers Component.For() .ImplementedBy() .LifeStyle.Transient); - + + container.Register( + Component.For() + .ImplementedBy() + .LifeStyle.Transient); + container.Register( Component.For().AsFactory());