Added auto updater

This commit is contained in:
Ben 2015-07-02 20:19:32 +01:00
parent 121681906d
commit eb054a5068
20 changed files with 455 additions and 7 deletions

View File

@ -48,8 +48,10 @@
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Repositories\TestItemFilterScriptRepository.cs" />
<Compile Include="Services\TestHTTPService.cs" />
<Compile Include="Services\TestItemFilterPersistenceService.cs" />
<Compile Include="Services\TestStaticDataService.cs" />
<Compile Include="Services\TestUpdateService.cs" />
<Compile Include="Translators\TestBlockGroupHierarchyBuilder.cs" />
<Compile Include="Translators\TestItemFilterBlockTranslator.cs" />
<Compile Include="Translators\TestItemFilterScriptTranslator.cs" />

View File

@ -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);
}
}
}

View File

@ -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 = @"<UpdateData>
<CurrentVersion>0.2</CurrentVersion>
<ReleaseDate>2015-07-01</ReleaseDate>
<DownloadUrl>http://www.google.com</DownloadUrl>
<ReleaseNotes>* Release notes line 1
* Release notes line 2
* More really great release notes!</ReleaseNotes>
</UpdateData>";
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<IHTTPService>();
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);
}
}
}

View File

@ -16,6 +16,12 @@
<setting name="ExtraLineBetweenBlocks" serializeAs="String">
<value>True</value>
</setting>
<setting name="SuppressUpdatesUpToVersion" serializeAs="String">
<value>0</value>
</setting>
<setting name="SuppressUpdates" serializeAs="String">
<value>False</value>
</setting>
</Filtration.Properties.Settings>
</userSettings>
<runtime>

View File

@ -139,10 +139,13 @@
<Compile Include="Converters\TreeViewMarginConverter.cs" />
<Compile Include="Extensions\EnumerationExtension.cs" />
<Compile Include="Extensions\HyperlinkExtensions.cs" />
<Compile Include="Models\UpdateData.cs" />
<Compile Include="Properties\Annotations.cs" />
<Compile Include="Repositories\ItemFilterScriptRepository.cs" />
<Compile Include="Services\HTTPService.cs" />
<Compile Include="Services\ItemFilterPersistenceService.cs" />
<Compile Include="Services\StaticDataService.cs" />
<Compile Include="Services\UpdateCheckService.cs" />
<Compile Include="Settings.cs" />
<Compile Include="Translators\BlockGroupHierarchyBuilder.cs" />
<Compile Include="Translators\ItemFilterBlockTranslator.cs" />
@ -172,6 +175,7 @@
<Compile Include="ViewModels\ToolPanes\SectionBrowserViewModel.cs" />
<Compile Include="ViewModels\StartPageViewModel.cs" />
<Compile Include="ViewModels\ToolPanes\ToolViewModel.cs" />
<Compile Include="ViewModels\UpdateAvailableViewModel.cs" />
<Compile Include="Views\AttachedProperties\SelectingItemAttachedProperty.cs" />
<Compile Include="Views\AvalonDock\AvalonDockWorkspaceView.xaml.cs">
<DependentUpon>AvalonDockWorkspaceView.xaml</DependentUpon>
@ -223,6 +227,9 @@
<Compile Include="Views\StartPageView.xaml.cs">
<DependentUpon>StartPageView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\UpdateAvailableView.xaml.cs">
<DependentUpon>UpdateAvailableView.xaml</DependentUpon>
</Compile>
<Compile Include="WindsorInstallers\ModelsInstaller.cs" />
<Compile Include="WindsorInstallers\RepositoriesInstaller.cs" />
<Compile Include="WindsorInstallers\ServicesInstaller.cs" />
@ -330,6 +337,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\UpdateAvailableView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
@ -397,7 +408,6 @@
<Resource Include="Resources\Icons\PasteStyle.ico" />
<Resource Include="Resources\Icons\ReplaceColors.ico" />
<Resource Include="Resources\Icons\Theme.ico" />
<Resource Include="Resources\Images\doge.jpg" />
<Resource Include="Resources\Icons\filtration_icon.png" />
<Content Include="Resources\ItemBaseTypes.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>

View File

@ -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("(?<!\r)\n");
_releaseNotes = r.Replace(value, "\r\n");
}
}
}
}

View File

@ -46,5 +46,29 @@ namespace Filtration.Properties {
this["ExtraLineBetweenBlocks"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("0")]
public decimal SuppressUpdatesUpToVersion {
get {
return ((decimal)(this["SuppressUpdatesUpToVersion"]));
}
set {
this["SuppressUpdatesUpToVersion"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool SuppressUpdates {
get {
return ((bool)(this["SuppressUpdates"]));
}
set {
this["SuppressUpdates"] = value;
}
}
}
}

View File

@ -8,5 +8,11 @@
<Setting Name="ExtraLineBetweenBlocks" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="SuppressUpdatesUpToVersion" Type="System.Decimal" Scope="User">
<Value Profile="(Default)">0</Value>
</Setting>
<Setting Name="SuppressUpdates" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
</Settings>
</SettingsFile>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

View File

@ -0,0 +1,31 @@
using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;
namespace Filtration.Services
{
internal interface IHTTPService
{
Task<string> GetContentAsync(string url);
}
internal class HTTPService : IHTTPService
{
public async Task<string> 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();
}
}
}
}
}

View File

@ -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<UpdateData> 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<UpdateData> 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;
}
}
}

View File

@ -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

View File

@ -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)
{

View File

@ -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());
}
}
}
}

View File

@ -28,8 +28,7 @@
<fluent:BackstageTabItem.ContentTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Margin="20">One day there will be something here! Maybe recent documents or something? For now here's a picture of Doge:</TextBlock>
<Image Source="/Filtration;component/Resources/Images/doge.jpg" Margin="20" Width="300" Height="300" />
<TextBlock Margin="20">Recent Documents will go here in a future release.</TextBlock>
</StackPanel>
</DataTemplate>
</fluent:BackstageTabItem.ContentTemplate>

View File

@ -25,11 +25,15 @@
<RowDefinition Height="5" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0">Default Filter Directory:</TextBlock>
<TextBox Grid.Row="0" Grid.Column="2" Text="{Binding DefaultFilterDirectory}" Width="250" />
<TextBlock Grid.Row="2" Grid.Column="0">Add blank line between blocks when saving</TextBlock>
<CheckBox Grid.Row="2" Grid.Column="2" IsChecked="{Binding ExtraLineBetweenBlocks}" />
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center">Add blank line between blocks when saving</TextBlock>
<CheckBox Grid.Row="2" Grid.Column="2" IsChecked="{Binding ExtraLineBetweenBlocks}" />
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center">Suppress update notifications for current version</TextBlock>
<CheckBox Grid.Row="4" Grid.Column="2" IsChecked="{Binding SuppressUpdateNotifications}" />
</Grid>
<StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
<StackPanel.Resources>

View File

@ -0,0 +1,38 @@
<Window x:Class="Filtration.Views.UpdateAvailableView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModels="clr-namespace:Filtration.ViewModels"
Title="Filtration - New Version Available!" Height="300" Width="500"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=viewModels:UpdateAvailableViewModel}">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Text="Current Version:" />
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding CurrentVersion}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="New Version:" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding NewVersion}" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="Release Date:" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="{Binding ReleaseDate, StringFormat={}{0:yyyy-MM-dd}}" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="Release Notes:" />
<TextBox MaxHeight="150" IsReadOnly="True" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Hidden" Grid.Row="3" Grid.Column="2" Text="{Binding ReleaseNotes, Mode=OneWay}" TextWrapping="Wrap" />
<StackPanel Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" Orientation="Horizontal" Height="30" HorizontalAlignment="Center" VerticalAlignment="Bottom" Margin="0,0,0,5">
<Button Padding="2" Margin="0,0,5,0" Command="{Binding DownloadCommand}">Download from Filtration homepage</Button>
<Button Padding="2" Margin="0,0,5,0" Command="{Binding AskLaterCommand}">Ask me later</Button>
<Button Padding="2" Margin="0,0,5,0" Command="{Binding NeverAskAgainCommand}">Never ask again for this version</Button>
</StackPanel>
</Grid>
</Window>

View File

@ -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
{
/// <summary>
/// Interaction logic for UpdateAvailableView.xaml
/// </summary>
public partial class UpdateAvailableView : Window
{
public UpdateAvailableView()
{
InitializeComponent();
}
}
}

View File

@ -24,6 +24,16 @@ namespace Filtration.WindsorInstallers
Component.For<IBlockGroupMapper>()
.ImplementedBy<BlockGroupMapper>()
.LifeStyle.Singleton);
container.Register(
Component.For<IHTTPService>()
.ImplementedBy<HTTPService>()
.LifeStyle.Singleton);
container.Register(
Component.For<IUpdateCheckService>()
.ImplementedBy<UpdateCheckService>()
.LifeStyle.Singleton);
}
}
}

View File

@ -60,7 +60,12 @@ namespace Filtration.WindsorInstallers
Component.For<ISettingsPageViewModel>()
.ImplementedBy<SettingsPageViewModel>()
.LifeStyle.Transient);
container.Register(
Component.For<IUpdateAvailableViewModel>()
.ImplementedBy<UpdateAvailableViewModel>()
.LifeStyle.Transient);
container.Register(
Component.For<IItemFilterBlockViewModelFactory>().AsFactory());