diff --git a/Docs/Images/MMExNotifier-MainWindow.png b/Docs/Images/MMExNotifier-MainWindow.png
new file mode 100644
index 0000000..4762161
Binary files /dev/null and b/Docs/Images/MMExNotifier-MainWindow.png differ
diff --git a/MMExNotifier.Tests/MMExNotifier.Tests.csproj b/MMExNotifier.Tests/MMExNotifier.Tests.csproj
new file mode 100644
index 0000000..4bbbc7b
--- /dev/null
+++ b/MMExNotifier.Tests/MMExNotifier.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net6.0-windows10.0.22000.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MMExNotifier.Tests/MainViewModelTests.cs b/MMExNotifier.Tests/MainViewModelTests.cs
new file mode 100644
index 0000000..9fc9b09
--- /dev/null
+++ b/MMExNotifier.Tests/MainViewModelTests.cs
@@ -0,0 +1,124 @@
+
+using MMExNotifier.Database;
+using MMExNotifier.DataModel;
+using MMExNotifier.Helpers;
+using MMExNotifier.ViewModels;
+using Moq;
+
+namespace MMExNotifier.Tests
+{
+ public class MainViewModelTests
+ {
+ private Mock mockAppConfiguration;
+ private Mock mockDatabaseService;
+ private Mock mockNotificationService;
+
+ [SetUp]
+ public void Setup()
+ {
+ mockAppConfiguration = new Mock();
+ mockAppConfiguration.Setup(x => x.MMExDatabasePath).Returns("C:\\mydatabase.mmb");
+ mockAppConfiguration.Setup(x => x.RunAtLogon).Returns(false);
+ mockAppConfiguration.Setup(x => x.DaysAhead).Returns(7);
+
+ mockDatabaseService = new Mock();
+
+ mockNotificationService = new Mock();
+ }
+
+ [Test]
+ public void WhenDatabasePathIsNotConfigured_ShouldNotAttemptOpenDatabase()
+ {
+ mockAppConfiguration.Setup(x => x.MMExDatabasePath).Returns(string.Empty);
+ mockDatabaseService.Setup(x => x.ExpiringBills).Returns(() => null);
+
+ var mainViewModel = new MainViewModel(mockAppConfiguration.Object, mockNotificationService.Object, mockDatabaseService.Object);
+ mainViewModel.Activate();
+
+ Assert.That(mockDatabaseService.Invocations, Is.Empty);
+ }
+
+ [Test]
+ public void WhenNoExpiringBills_ShouldRaiseCloseEvent()
+ {
+ mockDatabaseService.Setup(x => x.ExpiringBills).Returns(() => null);
+
+ var mainViewModel = new MainViewModel(mockAppConfiguration.Object, mockNotificationService.Object, mockDatabaseService.Object);
+ var closeInvoked = false;
+ mainViewModel.OnClose += (s, e) => { closeInvoked = true; };
+ mainViewModel.Activate();
+
+ Assert.That(closeInvoked, Is.True);
+ }
+
+ [Test]
+ public void WhenAtLeastOneExpiringBill_ShouldShowToastNotification()
+ {
+ mockDatabaseService.Setup(x => x.ExpiringBills).Returns(
+ new List
+ {
+ new()
+ {
+ BillId=1,
+ CategoryName="testCategory",
+ PayeeName="TestPayee",
+ NextOccurrenceDate=new DateTime(2024,6,1)
+ }
+ });
+
+ mockNotificationService.Setup(
+ x => x.ShowToastNotification("viewTransactions", It.IsAny(), "MMExNotifier", "One ore more recurring transaction are about to expire.", It.IsAny())
+ );
+
+ var mainViewModel = new MainViewModel(mockAppConfiguration.Object, mockNotificationService.Object, mockDatabaseService.Object);
+ mainViewModel.Activate();
+
+ mockNotificationService.Verify();
+ }
+
+ [Test]
+ public void WhenNotificationActivated_ShouldRaiseOpen()
+ {
+ mockDatabaseService.Setup(x => x.ExpiringBills).Returns(
+ new List
+ {
+ new()
+ {
+ BillId=1,
+ CategoryName="testCategory",
+ PayeeName="TestPayee",
+ NextOccurrenceDate=new DateTime(2024,6,1)
+ }
+ });
+
+ var mockToastNotification = new Mock();
+ mockToastNotification.Setup(
+ x => x.Show(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())
+ );
+
+ var mainViewModel = new MainViewModel(mockAppConfiguration.Object, new NotificationService(mockToastNotification.Object), mockDatabaseService.Object);
+ var openInvoked = false;
+ mainViewModel.OnOpen += (s, e) => { openInvoked = true; };
+ mainViewModel.Activate();
+
+ mockToastNotification.Raise(x => x.OnActivated += null, new EventArgs());
+
+ Assert.That(openInvoked, Is.True);
+ }
+
+ [Test]
+ public void OnDatabaseError_ShouldShowErrorMessage()
+ {
+ mockDatabaseService.Setup(x => x.ExpiringBills).Throws();
+
+ mockNotificationService.Setup(
+ x => x.ShowErrorNotification(It.IsAny())
+ );
+
+ var mainViewModel = new MainViewModel(mockAppConfiguration.Object, mockNotificationService.Object, mockDatabaseService.Object);
+ mainViewModel.Activate();
+
+ mockNotificationService.Verify();
+ }
+ }
+}
\ No newline at end of file
diff --git a/MMExNotifier.sln b/MMExNotifier.sln
index 4a3dbfc..faf5e82 100644
--- a/MMExNotifier.sln
+++ b/MMExNotifier.sln
@@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32228.430
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MMExNotifier", "MMExNotifier\MMExNotifier.csproj", "{91E2037D-BD50-4051-AA06-C8CC19D00131}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MMExNotifier", "MMExNotifier\MMExNotifier.csproj", "{91E2037D-BD50-4051-AA06-C8CC19D00131}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MMExNotifier.Tests", "MMExNotifier.Tests\MMExNotifier.Tests.csproj", "{8E1F80AF-5A10-4790-8367-A15EC5A0C2B2}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -15,6 +17,10 @@ Global
{91E2037D-BD50-4051-AA06-C8CC19D00131}.Debug|Any CPU.Build.0 = Debug|Any CPU
{91E2037D-BD50-4051-AA06-C8CC19D00131}.Release|Any CPU.ActiveCfg = Release|Any CPU
{91E2037D-BD50-4051-AA06-C8CC19D00131}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E1F80AF-5A10-4790-8367-A15EC5A0C2B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E1F80AF-5A10-4790-8367-A15EC5A0C2B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E1F80AF-5A10-4790-8367-A15EC5A0C2B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E1F80AF-5A10-4790-8367-A15EC5A0C2B2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/MMExNotifier/App.xaml.cs b/MMExNotifier/App.xaml.cs
index 8bb51a3..78738d9 100644
--- a/MMExNotifier/App.xaml.cs
+++ b/MMExNotifier/App.xaml.cs
@@ -1,7 +1,8 @@
-using Microsoft.Toolkit.Uwp.Notifications;
-using System.Linq;
+using MMExNotifier.Database;
+using MMExNotifier.DataModel;
+using MMExNotifier.Helpers;
+using MMExNotifier.ViewModels;
using System.Windows;
-using Windows.Foundation.Collections;
namespace MMExNotifier
{
@@ -14,43 +15,15 @@ protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
- var dbPath = MMExNotifier.Properties.Settings.Default.MMExDatabasePath;
+ var appConfiguration = new AppConfiguration();
- if (string.IsNullOrEmpty(dbPath))
- {
- var mainWindow = new MainWindow();
- return;
- }
+ var view = new MainWindow();
+ var viewModel = new MainViewModel(appConfiguration, new NotificationService(new ToastNotification()), new DatabaseService(appConfiguration));
- var daysAhead = MMExNotifier.Properties.Settings.Default.DaysAhead;
- var expiringTransactions = DbHelper.LoadRecurringTransactions(dbPath, daysAhead);
-
- if ((expiringTransactions != null) && (!expiringTransactions.Any()))
- {
- App.Current.Shutdown(0);
- }
- else
- {
- new ToastContentBuilder()
- .AddArgument("action", "viewTransactions")
- .AddArgument("conversationId", 9813)
- .AddText($"MMExNotifier", AdaptiveTextStyle.Header)
- .AddText($"One ore more recurring transaction are about to expire.")
- .SetToastScenario(ToastScenario.Reminder)
- .Show();
-
- // Listen to notification activation
- ToastNotificationManagerCompat.OnActivated += toastArgs =>
- {
- ToastArguments args = ToastArguments.Parse(toastArgs.Argument);
- ValueSet userInput = toastArgs.UserInput;
- Application.Current.Dispatcher.Invoke(delegate
- {
- var mainWindow = new MainWindow();
- mainWindow.ShowDialog();
- });
- };
- }
+ view.DataContext = viewModel;
+ viewModel.OnClose += (s, e) => Application.Current.Dispatcher.Invoke(() => view.Close());
+ viewModel.OnOpen += (s, e) => Application.Current.Dispatcher.Invoke(() => { if (view.Visibility != Visibility.Visible) view.ShowDialog(); });
+ viewModel.Activate();
}
}
}
diff --git a/MMExNotifier/AssemblyInfo.cs b/MMExNotifier/AssemblyInfo.cs
index 8b5504e..7b452a4 100644
--- a/MMExNotifier/AssemblyInfo.cs
+++ b/MMExNotifier/AssemblyInfo.cs
@@ -1,3 +1,4 @@
+using System.Runtime.CompilerServices;
using System.Windows;
[assembly: ThemeInfo(
@@ -8,3 +9,6 @@
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+
+[assembly: InternalsVisibleTo("MMExNotifier.Tests")]
+[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
diff --git a/MMExNotifier/Connection.cs b/MMExNotifier/Connection.cs
deleted file mode 100644
index 51b194a..0000000
--- a/MMExNotifier/Connection.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using LinqToDB.Data;
-
-namespace MMExNotifier
-{
- internal class Connection : DataConnection
- {
- public Connection()
- :base()
- {
-
- }
- }
-}
diff --git a/MMExNotifier/Entities/Account.cs b/MMExNotifier/DataModel/Account.cs
similarity index 97%
rename from MMExNotifier/Entities/Account.cs
rename to MMExNotifier/DataModel/Account.cs
index 52fef71..59ff585 100644
--- a/MMExNotifier/Entities/Account.cs
+++ b/MMExNotifier/DataModel/Account.cs
@@ -1,7 +1,7 @@
using LinqToDB.Mapping;
using System;
-namespace MMExNotifier.Entities
+namespace MMExNotifier.DataModel
{
[Table(Name = "ACCOUNTLIST_V1")]
internal class Account
diff --git a/MMExNotifier/DataModel/AppConfiguration.cs b/MMExNotifier/DataModel/AppConfiguration.cs
new file mode 100644
index 0000000..3de207a
--- /dev/null
+++ b/MMExNotifier/DataModel/AppConfiguration.cs
@@ -0,0 +1,71 @@
+using Microsoft.Win32.TaskScheduler;
+using System;
+using System.Linq;
+using System.Security.Principal;
+
+namespace MMExNotifier.DataModel
+{
+ internal class AppConfiguration : IAppConfiguration
+ {
+ public string? MMExDatabasePath { get; set; }
+ public int DaysAhead { get; set; }
+ public bool RunAtLogon { get; set; }
+
+ public AppConfiguration()
+ {
+ MMExDatabasePath = Properties.Settings.Default.MMExDatabasePath;
+ DaysAhead = Properties.Settings.Default.DaysAhead;
+
+ using TaskService taskService = new();
+ RunAtLogon = taskService.RootFolder.Tasks.Any(t => t.Name == "MMExNotifier");
+ }
+
+ public void Save()
+ {
+ Properties.Settings.Default.DaysAhead = DaysAhead;
+ Properties.Settings.Default.MMExDatabasePath = MMExDatabasePath;
+
+ if (RunAtLogon)
+ {
+ EnableSchedulerTask();
+ }
+ else
+ {
+ DisableSchedulerTask();
+ }
+
+ Properties.Settings.Default.Save();
+ }
+
+ private static void EnableSchedulerTask()
+ {
+ using TaskService taskService = new();
+
+ if (taskService.RootFolder.Tasks.Any(t => t.Name == "MMExNotifier"))
+ return;
+
+ TaskDefinition taskDefinition = taskService.NewTask();
+
+ // Set the task settings
+ taskDefinition.RegistrationInfo.Description = "MMExNotifier";
+ var userId = WindowsIdentity.GetCurrent().Name;
+
+ // Set the trigger to run on logon
+ LogonTrigger logonTrigger = new() { UserId = userId };
+ taskDefinition.Triggers.Add(logonTrigger);
+
+ // Set the action to run the executable that creates the task
+ string executablePath = Environment.ProcessPath ?? "";
+ taskDefinition.Actions.Add(new ExecAction(executablePath));
+
+ // Register the task in the Windows Task Scheduler
+ taskService.RootFolder.RegisterTaskDefinition("MMExNotifier", taskDefinition);
+ }
+
+ private static void DisableSchedulerTask()
+ {
+ using TaskService taskService = new();
+ taskService.RootFolder.DeleteTask("MMExNotifier", false);
+ }
+ }
+}
diff --git a/MMExNotifier/Entities/BillDeposit.cs b/MMExNotifier/DataModel/BillDeposit.cs
similarity index 97%
rename from MMExNotifier/Entities/BillDeposit.cs
rename to MMExNotifier/DataModel/BillDeposit.cs
index 1042a30..962c961 100644
--- a/MMExNotifier/Entities/BillDeposit.cs
+++ b/MMExNotifier/DataModel/BillDeposit.cs
@@ -1,7 +1,7 @@
using LinqToDB.Mapping;
using System;
-namespace MMExNotifier.Entities
+namespace MMExNotifier.DataModel
{
[Table(Name = "BILLSDEPOSITS_V1")]
internal class BillDeposit
diff --git a/MMExNotifier/Entities/Category.cs b/MMExNotifier/DataModel/Category.cs
similarity index 82%
rename from MMExNotifier/Entities/Category.cs
rename to MMExNotifier/DataModel/Category.cs
index f7bdbfc..a323a46 100644
--- a/MMExNotifier/Entities/Category.cs
+++ b/MMExNotifier/DataModel/Category.cs
@@ -1,8 +1,8 @@
using LinqToDB.Mapping;
-namespace MMExNotifier.Entities
+namespace MMExNotifier.DataModel
{
- [Table(Name="CATEGORY_V1")]
+ [Table(Name = "CATEGORY_V1")]
internal class Category
{
[PrimaryKey]
diff --git a/MMExNotifier/Entities/ExpiringBill.cs b/MMExNotifier/DataModel/ExpiringBill.cs
similarity index 93%
rename from MMExNotifier/Entities/ExpiringBill.cs
rename to MMExNotifier/DataModel/ExpiringBill.cs
index 5ea4944..8fbf123 100644
--- a/MMExNotifier/Entities/ExpiringBill.cs
+++ b/MMExNotifier/DataModel/ExpiringBill.cs
@@ -1,6 +1,6 @@
using System;
-namespace MMExNotifier.Entities
+namespace MMExNotifier.DataModel
{
public class ExpiringBill
{
diff --git a/MMExNotifier/DataModel/IAppConfiguration.cs b/MMExNotifier/DataModel/IAppConfiguration.cs
new file mode 100644
index 0000000..11fffb6
--- /dev/null
+++ b/MMExNotifier/DataModel/IAppConfiguration.cs
@@ -0,0 +1,11 @@
+namespace MMExNotifier.DataModel
+{
+ internal interface IAppConfiguration
+ {
+ int DaysAhead { get; set; }
+ string? MMExDatabasePath { get; set; }
+ bool RunAtLogon { get; set; }
+
+ void Save();
+ }
+}
\ No newline at end of file
diff --git a/MMExNotifier/Entities/Payee.cs b/MMExNotifier/DataModel/Payee.cs
similarity index 83%
rename from MMExNotifier/Entities/Payee.cs
rename to MMExNotifier/DataModel/Payee.cs
index a5a7aaf..b20ef1e 100644
--- a/MMExNotifier/Entities/Payee.cs
+++ b/MMExNotifier/DataModel/Payee.cs
@@ -1,8 +1,8 @@
using LinqToDB.Mapping;
-namespace MMExNotifier.Entities
+namespace MMExNotifier.DataModel
{
- [Table(Name="PAYEE_V1")]
+ [Table(Name = "PAYEE_V1")]
internal class Payee
{
[PrimaryKey]
diff --git a/MMExNotifier/Entities/Transaction.cs b/MMExNotifier/DataModel/Transaction.cs
similarity index 97%
rename from MMExNotifier/Entities/Transaction.cs
rename to MMExNotifier/DataModel/Transaction.cs
index 79632f2..378479b 100644
--- a/MMExNotifier/Entities/Transaction.cs
+++ b/MMExNotifier/DataModel/Transaction.cs
@@ -1,7 +1,7 @@
using LinqToDB.Mapping;
using System;
-namespace MMExNotifier.Entities
+namespace MMExNotifier.DataModel
{
[Table(Name = "CHECKINGACCOUNT_V1")]
internal class Transaction
diff --git a/MMExNotifier/Database/DatabaseService.cs b/MMExNotifier/Database/DatabaseService.cs
new file mode 100644
index 0000000..ceb3773
--- /dev/null
+++ b/MMExNotifier/Database/DatabaseService.cs
@@ -0,0 +1,58 @@
+using LinqToDB;
+using MMExNotifier.DataModel;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MMExNotifier.Database
+{
+ internal class DatabaseService : IDatabaseService
+ {
+ private readonly IAppConfiguration _appConfiguration;
+
+ public DatabaseService(IAppConfiguration appConfiguration)
+ {
+ _appConfiguration = appConfiguration;
+ }
+
+ public List? ExpiringBills
+ {
+ get
+ {
+ try
+ {
+ var db = new LinqToDB.Data.DataConnection(
+ ProviderName.SQLite,
+ $"Data Source={_appConfiguration.MMExDatabasePath}");
+
+ var billDeposits = db.GetTable();
+ var categories = db.GetTable();
+ var payees = db.GetTable();
+ var transactions = db.GetTable();
+ var accounts = db.GetTable();
+
+ return (from b in billDeposits
+ from s in categories.Where(s => s.CATEGID == b.CATEGID).DefaultIfEmpty()
+ from c in categories.Where(c => c.CATEGID == s.PARENTID).DefaultIfEmpty()
+ from p in payees.Where(p => p.PAYEEID == b.PAYEEID).DefaultIfEmpty()
+ where b.NEXTOCCURRENCEDATE < DateTime.Now.AddDays(_appConfiguration.DaysAhead)
+ orderby b.NEXTOCCURRENCEDATE
+ select new ExpiringBill
+ {
+ BillId = b.BDID,
+ NextOccurrenceDate = b.NEXTOCCURRENCEDATE.Value,
+ PayeeName = p.PAYEENAME!,
+ CategoryName = c.CATEGNAME!,
+ SubCategoryName = s.CATEGNAME!,
+ Notes = b.NOTES!
+ }).ToList();
+ }
+ catch (Exception)
+ {
+ throw;
+ }
+ }
+ }
+
+ }
+}
diff --git a/MMExNotifier/Database/IDatabaseService.cs b/MMExNotifier/Database/IDatabaseService.cs
new file mode 100644
index 0000000..2aefdd9
--- /dev/null
+++ b/MMExNotifier/Database/IDatabaseService.cs
@@ -0,0 +1,10 @@
+using MMExNotifier.DataModel;
+using System.Collections.Generic;
+
+namespace MMExNotifier.Database
+{
+ internal interface IDatabaseService
+ {
+ List? ExpiringBills { get; }
+ }
+}
\ No newline at end of file
diff --git a/MMExNotifier/DbHelper.cs b/MMExNotifier/DbHelper.cs
deleted file mode 100644
index 3979158..0000000
--- a/MMExNotifier/DbHelper.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-using LinqToDB;
-using MMExNotifier.Entities;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace MMExNotifier
-{
- internal static class DbHelper
- {
- public static List? LoadRecurringTransactions(string mmexDatabasePath, int daysAhead)
- {
- try
- {
- var db = new LinqToDB.Data.DataConnection(
- ProviderName.SQLite,
- $"Data Source={mmexDatabasePath}");
-
- var billDeposits = db.GetTable();
- var categories = db.GetTable();
- var payees = db.GetTable();
- var transactions = db.GetTable();
- var accounts = db.GetTable();
-
- return (from b in billDeposits
- from s in categories.Where(s => s.CATEGID == b.CATEGID).DefaultIfEmpty()
- from c in categories.Where(c => c.CATEGID == s.PARENTID).DefaultIfEmpty()
- from p in payees.Where(p => p.PAYEEID == b.PAYEEID).DefaultIfEmpty()
- where b.NEXTOCCURRENCEDATE < DateTime.Now.AddDays(daysAhead)
- orderby b.NEXTOCCURRENCEDATE
- select new ExpiringBill
- {
- BillId = b.BDID,
- NextOccurrenceDate = b.NEXTOCCURRENCEDATE.Value,
- PayeeName = p.PAYEENAME!,
- CategoryName = c.CATEGNAME!,
- SubCategoryName = s.CATEGNAME!,
- Notes = b.NOTES!
- }).ToList();
- }
- catch (Exception)
- {
- throw;
- }
- }
- }
-}
diff --git a/MMExNotifier/Helpers/INotificationService.cs b/MMExNotifier/Helpers/INotificationService.cs
new file mode 100644
index 0000000..6a7ab2c
--- /dev/null
+++ b/MMExNotifier/Helpers/INotificationService.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace MMExNotifier.Helpers
+{
+ internal interface INotificationService
+ {
+ public void ShowToastNotification(string actionName, int conversationId, string headerText, string message, Action onToastClickAction);
+ public void ShowErrorNotification(string message);
+ }
+}
diff --git a/MMExNotifier/Helpers/IToastNotification.cs b/MMExNotifier/Helpers/IToastNotification.cs
new file mode 100644
index 0000000..a39c368
--- /dev/null
+++ b/MMExNotifier/Helpers/IToastNotification.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace MMExNotifier.Helpers
+{
+ internal interface IToastNotification
+ {
+ event EventHandler? OnActivated;
+ void Show(string actionName, int conversationId, string headerText, string message);
+ }
+}
\ No newline at end of file
diff --git a/MMExNotifier/Helpers/NotificationService.cs b/MMExNotifier/Helpers/NotificationService.cs
new file mode 100644
index 0000000..e6fd80f
--- /dev/null
+++ b/MMExNotifier/Helpers/NotificationService.cs
@@ -0,0 +1,26 @@
+using System;
+using System.Windows;
+
+namespace MMExNotifier.Helpers
+{
+ internal class NotificationService : INotificationService
+ {
+ private IToastNotification _toastNotification;
+
+ public NotificationService(IToastNotification toastNotification)
+ {
+ _toastNotification = toastNotification;
+ }
+
+ public void ShowErrorNotification(string message)
+ {
+ MessageBox.Show(message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+
+ public void ShowToastNotification(string actionName, int conversationId, string headerText, string message, Action onToastClickAction)
+ {
+ _toastNotification.OnActivated += (s, e) => onToastClickAction();
+ _toastNotification.Show(actionName, conversationId, headerText, message);
+ }
+ }
+}
diff --git a/MMExNotifier/Helpers/ToastNotification.cs b/MMExNotifier/Helpers/ToastNotification.cs
new file mode 100644
index 0000000..bb81719
--- /dev/null
+++ b/MMExNotifier/Helpers/ToastNotification.cs
@@ -0,0 +1,30 @@
+using Microsoft.Toolkit.Uwp.Notifications;
+using System;
+using Windows.Foundation.Collections;
+
+namespace MMExNotifier.Helpers
+{
+ internal class ToastNotification : IToastNotification
+ {
+ public event EventHandler? OnActivated;
+
+ public void Show(string actionName, int conversationId, string headerText, string message)
+ {
+ new ToastContentBuilder()
+ .AddArgument("action", actionName)
+ .AddArgument("conversationId", conversationId)
+ .AddText(headerText, AdaptiveTextStyle.Header)
+ .AddText(message)
+ .SetToastScenario(ToastScenario.Reminder)
+ .Show();
+
+ // Listen to notification activation
+ ToastNotificationManagerCompat.OnActivated += toastArgs =>
+ {
+ ToastArguments args = ToastArguments.Parse(toastArgs.Argument);
+ ValueSet userInput = toastArgs.UserInput;
+ OnActivated?.Invoke(this, new EventArgs());
+ };
+ }
+ }
+}
diff --git a/MMExNotifier/IsLessThanConverter.cs b/MMExNotifier/MVVM/IsLessThanConverter.cs
similarity index 95%
rename from MMExNotifier/IsLessThanConverter.cs
rename to MMExNotifier/MVVM/IsLessThanConverter.cs
index d5d3fb9..e4c5e72 100644
--- a/MMExNotifier/IsLessThanConverter.cs
+++ b/MMExNotifier/MVVM/IsLessThanConverter.cs
@@ -2,7 +2,7 @@
using System.Globalization;
using System.Windows.Data;
-namespace MMExNotifier
+namespace MMExNotifier.MVVM
{
internal class IsLessThanConverter : IValueConverter
{
diff --git a/MMExNotifier/MVVM/RangeObservableCollection.cs b/MMExNotifier/MVVM/RangeObservableCollection.cs
new file mode 100644
index 0000000..2e566ce
--- /dev/null
+++ b/MMExNotifier/MVVM/RangeObservableCollection.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+
+namespace MMExNotifier.MVVM
+{
+ internal class RangeObservableCollection : ObservableCollection
+ {
+ public void AddRange(IEnumerable items)
+ {
+ using (BlockReentrancy())
+ {
+ foreach (T item in items)
+ {
+ Items.Add(item);
+ }
+
+ OnPropertyChanged(new PropertyChangedEventArgs("Count"));
+ OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+ }
+ }
+}
diff --git a/MMExNotifier/MVVM/RelayCommand.cs b/MMExNotifier/MVVM/RelayCommand.cs
new file mode 100644
index 0000000..6709c4e
--- /dev/null
+++ b/MMExNotifier/MVVM/RelayCommand.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Windows.Input;
+
+namespace MMExNotifier.MVVM
+{
+ internal class RelayCommand : ICommand
+ {
+ private readonly Action? _execute = null;
+ private readonly Func? _canExecute = null;
+
+ public event EventHandler? CanExecuteChanged
+ {
+ add { CommandManager.RequerySuggested += value; }
+ remove { CommandManager.RequerySuggested -= value; }
+ }
+
+ public RelayCommand(Action actionToExecute)
+ {
+ _execute = actionToExecute ?? throw new ArgumentNullException(nameof(actionToExecute));
+ }
+
+ public RelayCommand(Action actionToExecute, Func executionEvaluator)
+ : this(actionToExecute)
+ {
+ _canExecute = executionEvaluator ?? throw new ArgumentNullException(nameof(executionEvaluator));
+ }
+
+ public bool CanExecute(object? parameter)
+ {
+ return _canExecute == null ? true : _canExecute.Invoke();
+ }
+
+ public void Execute(object? parameter)
+ {
+ _execute?.Invoke();
+ }
+ }
+
+ internal class RelayCommand : ICommand
+ {
+ private readonly Action? _execute = null;
+ private readonly Predicate? _canExecute = null;
+
+ public event EventHandler? CanExecuteChanged
+ {
+ add { CommandManager.RequerySuggested += value; }
+ remove { CommandManager.RequerySuggested -= value; }
+ }
+
+ public RelayCommand(Action actionToExecute)
+ {
+ _execute = actionToExecute ?? throw new ArgumentNullException(nameof(actionToExecute));
+ }
+
+ public RelayCommand(Action actionToExecute, Predicate executionEvaluator)
+ : this(actionToExecute)
+ {
+ _canExecute = executionEvaluator ?? throw new ArgumentNullException(nameof(executionEvaluator));
+ }
+
+ public bool CanExecute(object? parameter)
+ {
+ if (_canExecute == null)
+ return true;
+
+ if (parameter == null && typeof(T?).IsValueType)
+ return _canExecute.Invoke(default);
+
+ if (parameter == null || parameter is T?)
+ return _canExecute.Invoke((T?)parameter);
+
+ return false;
+ }
+
+ public void Execute(object? parameter)
+ {
+ _execute?.Invoke((T?)parameter);
+ }
+ }
+}
diff --git a/MMExNotifier/MVVM/ViewModelBase.cs b/MMExNotifier/MVVM/ViewModelBase.cs
new file mode 100644
index 0000000..0c2b581
--- /dev/null
+++ b/MMExNotifier/MVVM/ViewModelBase.cs
@@ -0,0 +1,30 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace MMExNotifier.MVVM
+{
+ internal abstract class ViewModelBase : INotifyPropertyChanged
+ {
+ public event EventHandler? OnClose;
+ public event EventHandler? OnOpen;
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+
+ protected virtual void Close()
+ {
+ OnClose?.Invoke(this, EventArgs.Empty);
+ }
+
+ protected virtual void Open()
+ {
+ OnOpen?.Invoke(this, EventArgs.Empty);
+ }
+
+ public abstract void Activate();
+ }
+}
diff --git a/MMExNotifier/ViewModels/MainViewModel.cs b/MMExNotifier/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..01b0235
--- /dev/null
+++ b/MMExNotifier/ViewModels/MainViewModel.cs
@@ -0,0 +1,71 @@
+using MMExNotifier.Database;
+using MMExNotifier.DataModel;
+using MMExNotifier.Helpers;
+using MMExNotifier.MVVM;
+using System;
+
+namespace MMExNotifier.ViewModels
+{
+ internal class MainViewModel : ViewModelBase
+ {
+ private readonly INotificationService _notificationService;
+ private readonly IDatabaseService _database;
+
+ public RangeObservableCollection ExpiringBills { get; set; } = new RangeObservableCollection();
+ public IAppConfiguration AppSettings { get; private set; }
+ public RelayCommand SaveSettingsCommand { get; private set; }
+
+ public MainViewModel(IAppConfiguration appSettings, INotificationService notificationService, IDatabaseService databaseService)
+ {
+ _notificationService = notificationService;
+ _database = databaseService;
+
+ AppSettings = appSettings;
+ OnPropertyChanged(nameof(AppSettings));
+
+ SaveSettingsCommand = new(() => SaveSettings());
+ }
+
+ public override void Activate()
+ {
+ if (string.IsNullOrEmpty(AppSettings.MMExDatabasePath))
+ return;
+
+ LoadExpiringBills();
+
+ if (ExpiringBills.Count == 0)
+ {
+ Close();
+ }
+
+ const int ConversationId = 9813;
+ _notificationService.ShowToastNotification("viewTransactions", ConversationId, "MMExNotifier", "One ore more recurring transaction are about to expire.", () => Open());
+ }
+
+ private void LoadExpiringBills()
+ {
+ try
+ {
+ ExpiringBills.Clear();
+ var expiringBills = _database.ExpiringBills;
+ if (expiringBills != null)
+ {
+ ExpiringBills.AddRange(expiringBills);
+ }
+ }
+ catch (Exception)
+ {
+ _notificationService.ShowErrorNotification("An error has occurred while loading the recurring transactions.\nMake sure that the database version matches the supported version.");
+ }
+ }
+
+ private void SaveSettings()
+ {
+ AppSettings.Save();
+
+ LoadExpiringBills();
+ }
+
+ }
+}
+
diff --git a/MMExNotifier/MainWindow.xaml b/MMExNotifier/Views/MainWindow.xaml
similarity index 88%
rename from MMExNotifier/MainWindow.xaml
rename to MMExNotifier/Views/MainWindow.xaml
index 9c001ba..d57e9f0 100644
--- a/MMExNotifier/MainWindow.xaml
+++ b/MMExNotifier/Views/MainWindow.xaml
@@ -4,9 +4,13 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MMExNotifier"
+ xmlns:mvvm="clr-namespace:MMExNotifier.MVVM"
+ xmlns:viewmodels="clr-namespace:MMExNotifier.ViewModels"
+ d:DataContext="{d:DesignInstance Type=viewmodels:MainViewModel}"
mc:Ignorable="d"
WindowStartupLocation="Manual"
- Visibility="Visible"
+ Visibility="Hidden"
+ d:Visibility="Visible"
Title="MoneyManagerEx Notifier"
Height="450" Width="800"
AllowsTransparency="True"
@@ -14,7 +18,7 @@
WindowStyle="None">
-
+
+
+
@@ -168,7 +183,7 @@
-
-
+
@@ -188,7 +203,7 @@
-
+
@@ -201,7 +216,7 @@
-
+
diff --git a/MMExNotifier/MainWindow.xaml.cs b/MMExNotifier/Views/MainWindow.xaml.cs
similarity index 63%
rename from MMExNotifier/MainWindow.xaml.cs
rename to MMExNotifier/Views/MainWindow.xaml.cs
index 25f4ab2..743af23 100644
--- a/MMExNotifier/MainWindow.xaml.cs
+++ b/MMExNotifier/Views/MainWindow.xaml.cs
@@ -1,14 +1,10 @@
-using MMExNotifier.Entities;
-using System;
-using System.Collections.Generic;
-using System.Linq;
+using System;
using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using Microsoft.Win32.TaskScheduler;
using System.Security.Principal;
using System.IO;
-using System.ComponentModel;
using Drawing = System.Drawing;
using System.Reflection;
@@ -17,42 +13,18 @@ namespace MMExNotifier
///
/// Interaction logic for MainWindow.xaml
///
- public partial class MainWindow : Window, INotifyPropertyChanged
+ public partial class MainWindow : Window
{
- public List? ExpiringBills { get; set; }
-
- public string MMExDatabasePath { get; set; }
- public int DaysAhead { get; set; }
-
- public bool RunAtLogon { get; set; }
-
-
public MainWindow()
{
- DaysAhead = Properties.Settings.Default.DaysAhead;
- MMExDatabasePath = Properties.Settings.Default.MMExDatabasePath;
- using TaskService taskService = new();
- RunAtLogon = taskService.RootFolder.Tasks.Any(t => t.Name == "MMExNotifier");
-
InitializeComponent();
- DataContext = this;
Left = Properties.Settings.Default.WindowPosition.X;
Top = Properties.Settings.Default.WindowPosition.Y;
Width = Properties.Settings.Default.WindowSize.Width;
Height = Properties.Settings.Default.WindowSize.Height;
-
- if (string.IsNullOrEmpty(MMExDatabasePath))
- {
- settingsPanel.Visibility = Visibility.Visible;
- OpenFile_Click(this, new RoutedEventArgs());
- }
-
- LoadRecurringTransactions();
}
- public event PropertyChangedEventHandler? PropertyChanged;
-
private void Settings_Click(object sender, RoutedEventArgs e)
{
settingsPanel.Visibility = Visibility.Visible;
@@ -84,30 +56,9 @@ private void Close_Click(object sender, RoutedEventArgs e)
private void SettingsPanelClose_Click(object sender, RoutedEventArgs e)
{
settingsPanel.Visibility = Visibility.Collapsed;
-
- Properties.Settings.Default.DaysAhead = DaysAhead;
- Properties.Settings.Default.MMExDatabasePath = MMExDatabasePath;
- Properties.Settings.Default.Save();
-
- LoadRecurringTransactions();
}
- private void LoadRecurringTransactions()
- {
- try
- {
- ExpiringBills = DbHelper.LoadRecurringTransactions(MMExDatabasePath, DaysAhead);
- }
- catch (Exception)
- {
- ExpiringBills = null;
- MessageBox.Show("An error has occurred while loading the recurring transactions.\nMake sure that the database version matches the supported version.", "Error", MessageBoxButton.OK, MessageBoxImage.Warning);
- }
- finally
- {
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExpiringBills)));
- }
- }
+
private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
@@ -150,13 +101,12 @@ private void OpenFile_Click(object sender, RoutedEventArgs e)
{
Title = "Select a MoneyManagerEx database file",
Filter = "MMEx Database (*.mmb)|*.mmb",
- InitialDirectory = Path.GetDirectoryName(MMExDatabasePath)
+ InitialDirectory = Path.GetDirectoryName(DbPathTextbox.Text)
};
if (openFileDialog.ShowDialog() == true)
{
- MMExDatabasePath = openFileDialog.FileName;
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MMExDatabasePath)));
+ DbPathTextbox.Text = openFileDialog.FileName;
}
}
}