From 9fb148cf487d2e11e6a7d1c06062a3b6cb9ef52b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20Erbsh=C3=A4u=C3=9Fer?= Date: Sat, 13 Jun 2020 20:07:42 +0200 Subject: [PATCH] Implemented tenant model. --- ucalc/BillingWindow.xaml | 19 +- ucalc/BillingWindow.xaml.cs | 24 +- ucalc/Constants.cs | 3 + ucalc/Controls/Converters.cs | 87 +++++++- ucalc/Controls/ErrorCounter.xaml | 7 +- ucalc/Controls/ErrorCounter.xaml.cs | 6 +- ucalc/Controls/ErrorIcon.xaml | 7 +- ucalc/Controls/ErrorIcon.xaml.cs | 34 +-- ucalc/Controls/Helpers.cs | 13 +- ucalc/Data/Billing.cs | 6 +- ucalc/FlatWindow.xaml | 8 +- ucalc/FlatWindow.xaml.cs | 6 +- ucalc/MainWindow.xaml | 1 + ucalc/MainWindow.xaml.cs | 4 +- ucalc/Models/AddressModel.cs | 39 ---- ucalc/Models/AddressProperty.cs | 20 ++ ucalc/Models/BankAccountModel.cs | 34 --- ucalc/Models/BankAccountProperty.cs | 18 ++ ucalc/Models/BillingModel.cs | 22 -- ucalc/Models/BillingProperty.cs | 29 +++ ucalc/Models/FlatModel.cs | 40 ---- ucalc/Models/FlatProperty.cs | 39 ++++ ucalc/Models/FlatsProperty.cs | 55 +++++ ucalc/Models/HouseModel.cs | 57 ----- ucalc/Models/HouseProperty.cs | 16 ++ ucalc/Models/LandlordModel.cs | 47 ---- ucalc/Models/LandlordProperty.cs | 24 ++ ucalc/Models/Model.cs | 294 +++++++++---------------- ucalc/Models/Properties.cs | 326 ++++++++++++++++++++++++++++ ucalc/Models/SalutationProperty.cs | 12 + ucalc/Models/TenantProperty.cs | 203 +++++++++++++++++ ucalc/Models/TenantsProperty.cs | 39 ++++ ucalc/NewWindow.xaml.cs | 2 +- ucalc/Pages/HousePage.xaml | 80 ++++--- ucalc/Pages/HousePage.xaml.cs | 14 +- ucalc/Pages/LandlordPage.xaml | 42 ++-- ucalc/Pages/LandlordPage.xaml.cs | 2 +- ucalc/Pages/SideBar.xaml | 9 +- ucalc/Pages/SideBar.xaml.cs | 2 +- ucalc/Pages/TenantsPage.xaml | 97 ++++++++- ucalc/Pages/TenantsPage.xaml.cs | 33 ++- ucalc/TenantWindow.xaml | 253 +++++++++++++++++++++ ucalc/TenantWindow.xaml.cs | 41 ++++ ucalc/ucalc.csproj | 6 + 44 files changed, 1544 insertions(+), 576 deletions(-) delete mode 100644 ucalc/Models/AddressModel.cs create mode 100644 ucalc/Models/AddressProperty.cs delete mode 100644 ucalc/Models/BankAccountModel.cs create mode 100644 ucalc/Models/BankAccountProperty.cs delete mode 100644 ucalc/Models/BillingModel.cs create mode 100644 ucalc/Models/BillingProperty.cs delete mode 100644 ucalc/Models/FlatModel.cs create mode 100644 ucalc/Models/FlatProperty.cs create mode 100644 ucalc/Models/FlatsProperty.cs delete mode 100644 ucalc/Models/HouseModel.cs create mode 100644 ucalc/Models/HouseProperty.cs delete mode 100644 ucalc/Models/LandlordModel.cs create mode 100644 ucalc/Models/LandlordProperty.cs create mode 100644 ucalc/Models/Properties.cs create mode 100644 ucalc/Models/SalutationProperty.cs create mode 100644 ucalc/Models/TenantProperty.cs create mode 100644 ucalc/Models/TenantsProperty.cs create mode 100644 ucalc/TenantWindow.xaml create mode 100644 ucalc/TenantWindow.xaml.cs diff --git a/ucalc/BillingWindow.xaml b/ucalc/BillingWindow.xaml index 88188a6..82044a2 100644 --- a/ucalc/BillingWindow.xaml +++ b/ucalc/BillingWindow.xaml @@ -17,26 +17,33 @@ + LoadCompleted="OnSideBarFrameLoadCompleted" + Focusable="False" /> + LoadCompleted="OnLandlordFrameLoadCompleted" + Focusable="False" /> + LoadCompleted="OnHouseFrameLoadCompleted" + Focusable="False" /> - + - + - + diff --git a/ucalc/BillingWindow.xaml.cs b/ucalc/BillingWindow.xaml.cs index a95181a..e496078 100644 --- a/ucalc/BillingWindow.xaml.cs +++ b/ucalc/BillingWindow.xaml.cs @@ -10,18 +10,16 @@ namespace UCalc { public partial class BillingWindow { - public Billing Billing { get; } - public BillingModel Model { get; } + public Model Model { get; } - public BillingWindow(Billing savedBilling, Billing billing) + public BillingWindow(Billing billing) { - Billing = billing; + Model = new Model(billing); + InitializeComponent(); - Model = new BillingModel(Billing); - Title = - $"MietRechner - Abrechnung von {billing.StartDate.ToShortDateString()} - {billing.EndDate.Date.ToShortDateString()}"; + $"MietRechner - Abrechnung von {billing.StartDate.ToString(Constants.DateFormat)} - {billing.EndDate.Date.ToString(Constants.DateFormat)}"; } private void OnClosed(object sender, EventArgs e) @@ -41,13 +39,21 @@ namespace UCalc private void OnLandlordFrameLoadCompleted(object sender, NavigationEventArgs e) { var page = (LandlordPage) ((Frame) sender).Content; - page.Model = Model.LandlordModel; + page.Landlord = Model.Root.Landlord; } private void OnHouseFrameLoadCompleted(object sender, NavigationEventArgs e) { var page = (HousePage) ((Frame) sender).Content; - page.Model = Model.HouseModel; + page.House = Model.Root.House; + page.ParentWindow = this; + } + + private void OnTenantsFrameLoadCompleted(object sender, NavigationEventArgs e) + { + var page = (TenantsPage) ((Frame) sender).Content; + page.Tenants = Model.Root.Tenants; + page.House = Model.Root.House; page.ParentWindow = this; } } diff --git a/ucalc/Constants.cs b/ucalc/Constants.cs index 088da6f..199cbe3 100644 --- a/ucalc/Constants.cs +++ b/ucalc/Constants.cs @@ -11,6 +11,9 @@ namespace UCalc public static readonly SolidColorBrush MainColor = Brushes.White; public static readonly SolidColorBrush SubMainColor = new SolidColorBrush(Color.FromRgb(0x00, 0x7A, 0xCC)); + public const string DecimalFormat = ".00"; + public const string DateFormat = "dd.MM.yyyy"; + public static readonly ImmutableList SalutationStrs = ((Salutation[]) Enum.GetValues(typeof(Salutation))).Select(value => value.AsString()).ToImmutableList(); } diff --git a/ucalc/Controls/Converters.cs b/ucalc/Controls/Converters.cs index d31dcdc..16f26dd 100644 --- a/ucalc/Controls/Converters.cs +++ b/ucalc/Controls/Converters.cs @@ -1,19 +1,20 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Windows; using System.Windows.Data; +using UCalc.Models; namespace UCalc.Controls { public class ErrorsToVisibilityConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return (((ICollection) value)?.Count ?? 0) == 0 ? Visibility.Collapsed : Visibility.Visible; } - public object ConvertBack(object value, Type targetType, object parameter, - System.Globalization.CultureInfo culture) + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new InvalidOperationException(); } @@ -23,31 +24,93 @@ namespace UCalc.Controls { private readonly ErrorsToVisibilityConverter _converter = new ErrorsToVisibilityConverter(); - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var result = _converter.Convert(value, targetType, parameter, culture) as Visibility?; return result == Visibility.Collapsed ? Visibility.Visible : Visibility.Collapsed; } - public object ConvertBack(object value, Type targetType, object parameter, - System.Globalization.CultureInfo culture) + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new InvalidOperationException(); } } - - public class ErrorCountToVisibilityConverter : IValueConverter + public class ErrorsToCountConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { - return (int?) value != 0 ? Visibility.Visible : Visibility.Collapsed; + return ((ICollection) value)?.Count ?? 0; } - public object ConvertBack(object value, Type targetType, object parameter, - System.Globalization.CultureInfo culture) + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new InvalidOperationException(); } } + + public class ErrorsToToolTipConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var errors = (IReadOnlyList) value; + + return (errors?.Count ?? 0) == 0 ? null : string.Join("\n", errors); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new InvalidOperationException(); + } + } + + public class FlatToRentedConverter : IValueConverter + { + public TenantProperty Tenant { get; set; } + + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return Tenant.RentedFlats.Contains((FlatProperty) value); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new InvalidOperationException(); + } + } + + public class EmptyMultiPropertyToVisibilityConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + return value?.Equals(0) ?? false ? Visibility.Visible : Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new InvalidOperationException(); + } + } + + public class DatePickerTextToDateTimeConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + var dateTime = (DateTime?) value; + + return dateTime == null ? "" : dateTime.Value.ToString(Constants.DateFormat); + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + var str = (string) value; + + if (DateTime.TryParseExact(str, Constants.DateFormat, null, DateTimeStyles.None, out var d)) + { + return d; + } + + return null; + } + } } \ No newline at end of file diff --git a/ucalc/Controls/ErrorCounter.xaml b/ucalc/Controls/ErrorCounter.xaml index deb2844..ec22e23 100644 --- a/ucalc/Controls/ErrorCounter.xaml +++ b/ucalc/Controls/ErrorCounter.xaml @@ -7,16 +7,17 @@ mc:Ignorable="d"> - + + + Visibility="{Binding Path=Property.Errors, RelativeSource={RelativeSource AncestorType=local:ErrorCounter}, Converter={StaticResource ErrorsToVisibilityConverter}}"> diff --git a/ucalc/Controls/ErrorCounter.xaml.cs b/ucalc/Controls/ErrorCounter.xaml.cs index a7071c0..56182f2 100644 --- a/ucalc/Controls/ErrorCounter.xaml.cs +++ b/ucalc/Controls/ErrorCounter.xaml.cs @@ -5,10 +5,10 @@ namespace UCalc.Controls { public partial class ErrorCounter { - public Model Model { get; set; } + public Property Property { get; set; } - public static readonly DependencyProperty ModelProperty = DependencyProperty.Register( - "Model", typeof(Model), typeof(ErrorCounter), new PropertyMetadata((Model) null)); + public static readonly DependencyProperty PropertyProperty = DependencyProperty.Register( + "Property", typeof(Property), typeof(ErrorCounter), new PropertyMetadata((Property) null)); public ErrorCounter() { diff --git a/ucalc/Controls/ErrorIcon.xaml b/ucalc/Controls/ErrorIcon.xaml index 6fdebc1..7743cde 100644 --- a/ucalc/Controls/ErrorIcon.xaml +++ b/ucalc/Controls/ErrorIcon.xaml @@ -11,11 +11,12 @@ + - + @@ -32,7 +33,7 @@ diff --git a/ucalc/Controls/ErrorIcon.xaml.cs b/ucalc/Controls/ErrorIcon.xaml.cs index 90cc4e7..ced9361 100644 --- a/ucalc/Controls/ErrorIcon.xaml.cs +++ b/ucalc/Controls/ErrorIcon.xaml.cs @@ -1,40 +1,18 @@ -using System.Collections.Generic; -using System.ComponentModel; -using System.Runtime.CompilerServices; -using System.Windows; -using UCalc.Annotations; +using System.Windows; +using UCalc.Models; namespace UCalc.Controls { - public partial class ErrorIcon : INotifyPropertyChanged + public partial class ErrorIcon { - public ICollection Errors { get; set; } + public Property Property { get; set; } - public string ErrorsToolTip { get; private set; } - - public static readonly DependencyProperty ErrorsProperty = DependencyProperty.Register( - "Errors", typeof(ICollection), typeof(ErrorIcon), - new PropertyMetadata(null, OnErrorsChanged)); + public static readonly DependencyProperty PropertyProperty = DependencyProperty.Register( + "Property", typeof(Property), typeof(ErrorIcon), new PropertyMetadata((Property) null)); public ErrorIcon() { InitializeComponent(); } - - private static void OnErrorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - var self = (ErrorIcon) d; - var newErrors = (ICollection) e.NewValue; - self.ErrorsToolTip = newErrors.Count > 0 ? string.Join("\n", newErrors) : null; - self.OnPropertyChanged("ErrorsToolTip"); - } - - public event PropertyChangedEventHandler PropertyChanged; - - [NotifyPropertyChangedInvocator] - private void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } } } \ No newline at end of file diff --git a/ucalc/Controls/Helpers.cs b/ucalc/Controls/Helpers.cs index 865f82d..c3cfab3 100644 --- a/ucalc/Controls/Helpers.cs +++ b/ucalc/Controls/Helpers.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System.Windows.Media; @@ -26,5 +27,15 @@ namespace UCalc.Controls path.Fill = brush; } } + + private static bool IsBetween(this DateTime dt, DateTime start, DateTime end) + { + return dt >= start && dt <= end; + } + + public static bool Intersects(this DateTime start1, DateTime end1, DateTime start2, DateTime end2) + { + return start1.IsBetween(start2, end2) || start2.IsBetween(start1, end1); + } } } \ No newline at end of file diff --git a/ucalc/Data/Billing.cs b/ucalc/Data/Billing.cs index dd9ac93..961b3a5 100644 --- a/ucalc/Data/Billing.cs +++ b/ucalc/Data/Billing.cs @@ -167,7 +167,7 @@ namespace UCalc.Data public int PersonCount { get; set; } public BankAccount BankAccount { get; private set; } public DateTime? EntryDate { get; set; } - public DateTime? DepatureDate { get; set; } + public DateTime? DepartureDate { get; set; } public HashSet RentedFlats { get; private set; } public decimal PaidRent { get; set; } public string CustomMessage1 { get; set; } @@ -185,7 +185,7 @@ namespace UCalc.Data { return Id.Equals(other.Id) && Salutation == other.Salutation && Name == other.Name && PersonCount == other.PersonCount && Equals(BankAccount, other.BankAccount) && - Nullable.Equals(EntryDate, other.EntryDate) && Nullable.Equals(DepatureDate, other.DepatureDate) && + Nullable.Equals(EntryDate, other.EntryDate) && Nullable.Equals(DepartureDate, other.DepartureDate) && RentedFlats.SequenceEqual(other.RentedFlats) && PaidRent == other.PaidRent && CustomMessage1 == other.CustomMessage1 && CustomMessage2 == other.CustomMessage2; } @@ -207,7 +207,7 @@ namespace UCalc.Data PersonCount = PersonCount, BankAccount = BankAccount.Clone(), EntryDate = EntryDate, - DepatureDate = DepatureDate, + DepartureDate = DepartureDate, RentedFlats = new HashSet(RentedFlats.Select(flat => flatMapper[flat])), PaidRent = PaidRent, CustomMessage1 = CustomMessage1, diff --git a/ucalc/FlatWindow.xaml b/ucalc/FlatWindow.xaml index a047440..667b5e0 100644 --- a/ucalc/FlatWindow.xaml +++ b/ucalc/FlatWindow.xaml @@ -43,12 +43,12 @@ + Property="{Binding Path=Flat.Name, RelativeSource={RelativeSource AncestorType=local:FlatWindow}}" /> + Text="{Binding Path=Flat.Name.Value, RelativeSource={RelativeSource AncestorType=local:FlatWindow}, UpdateSourceTrigger=PropertyChanged}" /> @@ -60,12 +60,12 @@ + Property="{Binding Path=Flat.Size, RelativeSource={RelativeSource AncestorType=local:FlatWindow}}" /> + Text="{Binding Path=Flat.Size.Value, RelativeSource={RelativeSource AncestorType=local:FlatWindow}, UpdateSourceTrigger=PropertyChanged}" /> diff --git a/ucalc/FlatWindow.xaml.cs b/ucalc/FlatWindow.xaml.cs index 440ddad..e4c0b7b 100644 --- a/ucalc/FlatWindow.xaml.cs +++ b/ucalc/FlatWindow.xaml.cs @@ -5,11 +5,11 @@ namespace UCalc { public partial class FlatWindow { - public FlatModel Model { get; } + public FlatProperty Flat { get; } - public FlatWindow(FlatModel model) + public FlatWindow(FlatProperty flat) { - Model = model; + Flat = flat; InitializeComponent(); } diff --git a/ucalc/MainWindow.xaml b/ucalc/MainWindow.xaml index f561b6a..a3c5280 100644 --- a/ucalc/MainWindow.xaml +++ b/ucalc/MainWindow.xaml @@ -8,6 +8,7 @@ Title="MietRechner" Height="400" Width="500" + WindowStartupLocation="CenterScreen" ResizeMode="CanMinimize"> diff --git a/ucalc/MainWindow.xaml.cs b/ucalc/MainWindow.xaml.cs index 0343b3c..74fdf7c 100644 --- a/ucalc/MainWindow.xaml.cs +++ b/ucalc/MainWindow.xaml.cs @@ -22,7 +22,7 @@ namespace UCalc if (newWindow.ShowDialog() == true) { - new BillingWindow(newWindow.SavedBilling, newWindow.Billing).Show(); + new BillingWindow(newWindow.Billing).Show(); Hide(); } } @@ -75,7 +75,7 @@ namespace UCalc var billing = new BillingLoader().Load(path); App.RecentlyOpenedList.Add(new RecentlyOpenedItem(path)); - new BillingWindow(billing, billing.Clone()).Show(); + new BillingWindow(billing).Show(); Hide(); } catch (IOException) diff --git a/ucalc/Models/AddressModel.cs b/ucalc/Models/AddressModel.cs deleted file mode 100644 index 43d1bd7..0000000 --- a/ucalc/Models/AddressModel.cs +++ /dev/null @@ -1,39 +0,0 @@ -using UCalc.Data; - -namespace UCalc.Models -{ - public class AddressModel : Model - { - private readonly Address _data; - public ModelProperty Street { get; } - public ModelProperty HouseNumber { get; } - public ModelProperty City { get; } - public ModelProperty Postcode { get; } - - public AddressModel(Address data) - { - _data = data; - - Street = Add(new ModelProperty("Straße", _data.Street, ModelPropertyValidators.IsNotEmpty)); - HouseNumber = - Add(new ModelProperty("Hausnummer", _data.HouseNumber, ModelPropertyValidators.IsNotEmpty)); - City = Add(new ModelProperty("Stadt", _data.City, ModelPropertyValidators.IsNotEmpty)); - Postcode = Add(new ModelProperty("PLZ", _data.Postcode, ModelPropertyValidators.IsNotEmpty)); - } - - public override void Apply() - { - _data.Street = Street.Value; - Street.ResetModified(); - - _data.HouseNumber = HouseNumber.Value; - HouseNumber.ResetModified(); - - _data.City = City.Value; - City.ResetModified(); - - _data.Postcode = Postcode.Value; - Postcode.ResetModified(); - } - } -} \ No newline at end of file diff --git a/ucalc/Models/AddressProperty.cs b/ucalc/Models/AddressProperty.cs new file mode 100644 index 0000000..80abc0b --- /dev/null +++ b/ucalc/Models/AddressProperty.cs @@ -0,0 +1,20 @@ +using UCalc.Data; + +namespace UCalc.Models +{ + public class AddressProperty : NestedProperty + { + public NotEmptyStringProperty Street { get; } + public NotEmptyStringProperty HouseNumber { get; } + public NotEmptyStringProperty City { get; } + public NotEmptyStringProperty Postcode { get; } + + public AddressProperty(Model model, Property parent, Address data) : base(model, parent) + { + Street = Add(new NotEmptyStringProperty(model, this, "Straße", data.Street)); + HouseNumber = Add(new NotEmptyStringProperty(model, this, "Hausnummer", data.HouseNumber)); + City = Add(new NotEmptyStringProperty(model, this, "Stadt", data.City)); + Postcode = Add(new NotEmptyStringProperty(model, this, "PLZ", data.Postcode)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/BankAccountModel.cs b/ucalc/Models/BankAccountModel.cs deleted file mode 100644 index 8160bc0..0000000 --- a/ucalc/Models/BankAccountModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using UCalc.Data; - -namespace UCalc.Models -{ - public class BankAccountModel : Model - { - private readonly BankAccount _data; - public ModelProperty Iban { get; } - public ModelProperty Bic { get; } - public ModelProperty BankName { get; } - - public BankAccountModel(BankAccount data) - { - _data = data; - - Iban = Add(new ModelProperty("IBAN", _data.Iban, ModelPropertyValidators.IsNotEmpty)); - Bic = Add(new ModelProperty("BIC", _data.Bic, ModelPropertyValidators.IsNotEmpty)); - BankName = Add(new ModelProperty("Name der Bank", _data.BankName, - ModelPropertyValidators.IsNotEmpty)); - } - - public override void Apply() - { - _data.Iban = Iban.Value; - Iban.ResetModified(); - - _data.Bic = Bic.Value; - Bic.ResetModified(); - - _data.BankName = BankName.Value; - BankName.ResetModified(); - } - } -} \ No newline at end of file diff --git a/ucalc/Models/BankAccountProperty.cs b/ucalc/Models/BankAccountProperty.cs new file mode 100644 index 0000000..dfac11c --- /dev/null +++ b/ucalc/Models/BankAccountProperty.cs @@ -0,0 +1,18 @@ +using UCalc.Data; + +namespace UCalc.Models +{ + public class BankAccountProperty : NestedProperty + { + public NotEmptyStringProperty Iban { get; } + public NotEmptyStringProperty Bic { get; } + public NotEmptyStringProperty BankName { get; } + + public BankAccountProperty(Model model, Property parent, BankAccount data) : base(model, parent) + { + Iban = Add(new NotEmptyStringProperty(model, this, "IBAN", data.Iban)); + Bic = Add(new NotEmptyStringProperty(model, this, "BIC", data.Bic)); + BankName = Add(new NotEmptyStringProperty(model, this, "Name der Bank", data.BankName)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/BillingModel.cs b/ucalc/Models/BillingModel.cs deleted file mode 100644 index 5d6bcc5..0000000 --- a/ucalc/Models/BillingModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -using UCalc.Data; - -namespace UCalc.Models -{ - public class BillingModel : Model - { - public LandlordModel LandlordModel { get; } - public HouseModel HouseModel { get; } - - public BillingModel(Billing billing) - { - LandlordModel = new LandlordModel(billing.Landlord); - HouseModel = new HouseModel(billing.House); - } - - public override void Apply() - { - LandlordModel.Apply(); - HouseModel.Apply(); - } - } -} \ No newline at end of file diff --git a/ucalc/Models/BillingProperty.cs b/ucalc/Models/BillingProperty.cs new file mode 100644 index 0000000..bcdf237 --- /dev/null +++ b/ucalc/Models/BillingProperty.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using UCalc.Data; + +namespace UCalc.Models +{ + public class BillingProperty : NestedProperty + { + public LandlordProperty Landlord { get; } + public HouseProperty House { get; } + public TenantsProperty Tenants { get; } + + + // TODO: Costs + + public BillingProperty(Model model, Property parent, Billing data) : base(model, parent) + { + Landlord = Add(new LandlordProperty(model, this, data.Landlord)); + House = Add(new HouseProperty(model, this, data.House)); + + var flatToProperty = new Dictionary(); + for (var i = 0; i < data.House.Flats.Count; ++i) + { + flatToProperty.Add(data.House.Flats[i], House.Flats[i]); + } + + Tenants = Add(new TenantsProperty(model, this, data.Tenants, flatToProperty)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/FlatModel.cs b/ucalc/Models/FlatModel.cs deleted file mode 100644 index 6cd98d1..0000000 --- a/ucalc/Models/FlatModel.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using UCalc.Data; - -namespace UCalc.Models -{ - public class FlatModel : Model - { - public Flat Data { get; } - public ModelProperty Name { get; } - public ModelProperty Size { get; } - - public FlatModel(Flat data, IReadOnlyList flats) - { - Data = data; - - Name = Add(new ModelProperty("Name", Data.Name, (name, value) => - { - var error = ModelPropertyValidators.IsNotEmpty(name, value); - - if (error == "" && flats.Any(flat => !ReferenceEquals(flat, Data) && flat.Name == value)) - { - error = $"{name}: Der Wert \"{value}\" ist nicht eindeutig."; - } - - return error; - })); - Size = Add(new ModelProperty("Größe", Data.Size.ToString(), ModelPropertyValidators.IsNaturalInt)); - } - - public override void Apply() - { - Data.Name = Name.Value; - Name.ResetModified(); - - Data.Size = int.TryParse(Size.Value, out var n) && n > 0 ? n : 0; - Size.ResetModified(); - } - } -} \ No newline at end of file diff --git a/ucalc/Models/FlatProperty.cs b/ucalc/Models/FlatProperty.cs new file mode 100644 index 0000000..f8f3027 --- /dev/null +++ b/ucalc/Models/FlatProperty.cs @@ -0,0 +1,39 @@ +using System.Linq; +using UCalc.Data; + +namespace UCalc.Models +{ + public class FlatNameProperty : ValueProperty + { + public FlatNameProperty(Model model, Property parent, string name, string value) : base(model, parent, name, + value) + { + } + + protected override string ValidateValue() + { + var error = ""; + using var validator = Model.BeginValidation(); + + if (Model.Root.House.Flats.Any(flat => !ReferenceEquals(this, flat.Name) && flat.Name.Value == Value)) + { + error = $"{Name}: Der Name ist nicht eindeutig."; + } + + validator.ValidateRange(Model.Root.House.Flats.Select(flat => flat.Name)); + return error; + } + } + + public class FlatProperty : NestedProperty + { + public FlatNameProperty Name { get; } + public NaturalNumberProperty Size { get; } + + public FlatProperty(Model model, Property parent, Flat data) : base(model, parent) + { + Name = Add(new FlatNameProperty(model, this, "Name", data.Name)); + Size = Add(new NaturalNumberProperty(model, this, "Größe", data.Size)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/FlatsProperty.cs b/ucalc/Models/FlatsProperty.cs new file mode 100644 index 0000000..fa74019 --- /dev/null +++ b/ucalc/Models/FlatsProperty.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Linq; +using UCalc.Data; + +namespace UCalc.Models +{ + public class FlatsProperty : MultiProperty + { + public FlatsProperty(Model model, Property parent, IEnumerable data) : base(model, parent, + "Wohnungen: Geben Sie eine oder mehr Wohnungen an.") + { + using var validator = Model.BeginValidation(); + + foreach (var flat in data) + { + base.Add(new FlatProperty(Model, this, flat)); + } + + Modified = false; + } + + public void Add() + { + FlatProperty flat; + { + using var validator = Model.BeginValidation(); + + flat = new FlatProperty(Model, this, new Flat {Name = $"Wohnung {Properties.Count + 1}"}); + base.Add(flat); + } + + { + using var validator = Model.BeginValidation(); + validator.Validate(flat); + } + } + + public new void Remove(FlatProperty flat) + { + using var validator = Model.BeginValidation(); + + base.Remove(flat); + + // Revalidate flat names and flat lists in tenants and costs + validator.ValidateRange(Properties.Select(otherFlat => otherFlat.Name)); + + foreach (var tenant in Model.Root.Tenants) + { + tenant.RentedFlats.Remove(flat); + } + + // TODO: Revalidate all flats + revalidate tenant rent lists + costs flat lists + } + } +} \ No newline at end of file diff --git a/ucalc/Models/HouseModel.cs b/ucalc/Models/HouseModel.cs deleted file mode 100644 index 9ad0cd0..0000000 --- a/ucalc/Models/HouseModel.cs +++ /dev/null @@ -1,57 +0,0 @@ -using UCalc.Data; - -namespace UCalc.Models -{ - public class HouseModel : Model - { - private readonly House _data; - public NestedModelProperty Address { get; } - public MultiModelProperty Flats { get; } - - public HouseModel(House data) - { - _data = data; - - Address = Add(new NestedModelProperty(new AddressModel(_data.Address))); - Flats = Add(new MultiModelProperty()); - - foreach (var flat in data.Flats) - { - Flats.Add(new FlatModel(flat, _data.Flats)); - } - } - - public override void Apply() - { - Address.Model.Apply(); - - foreach (var model in Flats.Models) - { - model.Apply(); - } - } - - public FlatModel AddFlat() - { - var flat = new Flat {Name = $"Wohnung {_data.Flats.Count + 1}"}; - _data.Flats.Add(flat); - - var model = new FlatModel(flat, _data.Flats); - Flats.Add(model); - - return model; - } - - public void RemoveFlat(FlatModel model) - { - _data.Flats.Remove(model.Data); - - Flats.Remove(model); - - if (model.Errors.Count > 0) - { - OnPropertyChanged("Errors"); - } - } - } -} \ No newline at end of file diff --git a/ucalc/Models/HouseProperty.cs b/ucalc/Models/HouseProperty.cs new file mode 100644 index 0000000..65bbcb0 --- /dev/null +++ b/ucalc/Models/HouseProperty.cs @@ -0,0 +1,16 @@ +using UCalc.Data; + +namespace UCalc.Models +{ + public class HouseProperty : NestedProperty + { + public AddressProperty Address { get; } + public FlatsProperty Flats { get; } + + public HouseProperty(Model model, Property parent, House data) : base(model, parent) + { + Address = Add(new AddressProperty(model, this, data.Address)); + Flats = Add(new FlatsProperty(model, this, data.Flats)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/LandlordModel.cs b/ucalc/Models/LandlordModel.cs deleted file mode 100644 index adf3658..0000000 --- a/ucalc/Models/LandlordModel.cs +++ /dev/null @@ -1,47 +0,0 @@ -using UCalc.Data; - -namespace UCalc.Models -{ - public class LandlordModel : Model - { - private readonly Landlord _data; - public ModelProperty Salutation { get; } - public ModelProperty Name { get; } - public ModelProperty MailAddress { get; } - public ModelProperty Phone { get; } - public NestedModelProperty Address { get; } - public NestedModelProperty BankAccount { get; } - - public LandlordModel(Landlord data) - { - _data = data; - - Salutation = Add(new ModelProperty("Anrede", (int) _data.Salutation, null)); - Name = Add(new ModelProperty("Name", _data.Name, ModelPropertyValidators.IsNotEmpty)); - MailAddress = - Add(new ModelProperty("Email Adresse", _data.MailAddress, ModelPropertyValidators.IsNotEmpty)); - Phone = Add(new ModelProperty("Telefonnummer", _data.Phone, ModelPropertyValidators.IsNotEmpty)); - Address = Add(new NestedModelProperty(new AddressModel(_data.Address))); - BankAccount = Add(new NestedModelProperty(new BankAccountModel(_data.BankAccount))); - } - - public override void Apply() - { - _data.Salutation = (Salutation) Salutation.Value; - Salutation.ResetModified(); - - _data.Name = Name.Value; - Name.ResetModified(); - - _data.MailAddress = MailAddress.Value; - MailAddress.ResetModified(); - - _data.Phone = Phone.Value; - Phone.ResetModified(); - - Address.Model.Apply(); - - BankAccount.Model.Apply(); - } - } -} \ No newline at end of file diff --git a/ucalc/Models/LandlordProperty.cs b/ucalc/Models/LandlordProperty.cs new file mode 100644 index 0000000..ea22858 --- /dev/null +++ b/ucalc/Models/LandlordProperty.cs @@ -0,0 +1,24 @@ +using UCalc.Data; + +namespace UCalc.Models +{ + public class LandlordProperty : NestedProperty + { + public SalutationProperty Salutation { get; } + public NotEmptyStringProperty Name { get; } + public NotEmptyStringProperty MailAddress { get; } + public NotEmptyStringProperty Phone { get; } + public AddressProperty Address { get; } + public BankAccountProperty BankAccount { get; } + + public LandlordProperty(Model model, Property parent, Landlord data) : base(model, parent) + { + Salutation = Add(new SalutationProperty(model, this, "Anrede", data.Salutation)); + Name = Add(new NotEmptyStringProperty(model, this, "Name", data.Name)); + MailAddress = Add(new NotEmptyStringProperty(model, this, "Email Adresse", data.MailAddress)); + Phone = Add(new NotEmptyStringProperty(model, this, "Telefonnummer", data.Phone)); + Address = Add(new AddressProperty(model, this, data.Address)); + BankAccount = Add(new BankAccountProperty(model, this, data.BankAccount)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/Model.cs b/ucalc/Models/Model.cs index c280799..378bdef 100644 --- a/ucalc/Models/Model.cs +++ b/ucalc/Models/Model.cs @@ -1,234 +1,152 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; -using System.Linq; using System.Runtime.CompilerServices; using UCalc.Annotations; +using UCalc.Data; namespace UCalc.Models { - public abstract class ModelBaseProperty : INotifyPropertyChanged + public class Model : INotifyPropertyChanged { - protected static readonly List EmptyList = new List(); - public abstract IReadOnlyCollection Errors { get; } - public abstract bool Modified { get; } - - protected void OnChildPropertyChanged(object sender, PropertyChangedEventArgs args) + public class Validator : IDisposable { - if (args.PropertyName == "Errors" || args.PropertyName == "Modified") + private readonly Model _model; + private int _counter; + private readonly HashSet _validated; + private readonly HashSet> _notifications; + + public Validator(Model model) { - OnPropertyChanged(args.PropertyName); + _model = model; + _validated = new HashSet(); + _notifications = new HashSet>(); } - } - public event PropertyChangedEventHandler PropertyChanged; - - [NotifyPropertyChangedInvocator] - protected void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } - } - - public class NestedModelProperty : ModelBaseProperty where T : Model - { - public T Model { get; } - - public NestedModelProperty(T model) - { - Model = model; - - Model.PropertyChanged += OnChildPropertyChanged; - } - - public override IReadOnlyCollection Errors => Model.Errors; - - public override bool Modified => Model.Modified; - } - - public class MultiModelProperty : ModelBaseProperty where T : Model - { - private readonly ObservableCollection _models; - - public MultiModelProperty() - { - _models = new ObservableCollection(); - } - - public void Add(T model) - { - _models.Add(model); - - model.PropertyChanged += OnChildPropertyChanged; - - if (model.Errors.Count > 0) + public void Notify(Property property, string propertyName) { - OnPropertyChanged("Errors"); - } - } + _notifications.Add(new Tuple(property, propertyName)); - public void Remove(T model) - { - model.PropertyChanged -= OnChildPropertyChanged; - - _models.Remove(model); - - if (model.Errors.Count > 0) - { - OnPropertyChanged("Errors"); - } - } - - public IReadOnlyList Models => _models; - - public override IReadOnlyCollection Errors - { - get - { - var errors = new List(); - - foreach (var model in _models) + if (property != null) { - errors.AddRange(model.Errors); + property = property.Parent; + + while (property != null) + { + _notifications.Add(new Tuple(property, propertyName)); + property = property.Parent; + } + + _notifications.Add(new Tuple(null, propertyName)); } - - return errors; } - } - public override bool Modified => _models.Select(model => model.Modified).Any(); - } - - public class ModelProperty : ModelBaseProperty - { - public string Name { get; } - private T _value; - private readonly List _errors; - private readonly Func _validate; - private bool _modified; - - public T Value - { - get => _value; - set + public void Validate(Property property) { - if (_value.Equals(value)) + if (property == null) { return; } - var oldError = _errors[0]; - _errors[0] = _validate?.Invoke(Name, value) ?? ""; - - if (oldError != _errors[0]) + if (_validated.Add(property)) { - OnPropertyChanged("Errors"); + property.Validate(); } + } - if (!_modified) + public void ValidateRange(IEnumerable properties) + { + foreach (var property in properties) { - OnPropertyChanged("Modified"); + Validate(property); } + } - _modified = true; - _value = value; + public void IncValidationCounter() + { + ++_counter; + } - OnPropertyChanged("Value"); + public void Dispose() + { + --_counter; + + if (_counter == 0) + { + _validated.Clear(); + + var sortedNotifications = new List>(_notifications); + sortedNotifications.Sort((x, y) => + { + var (prop1, propName1) = x; + var (prop2, propName2) = y; + + if (prop1 == null) + { + return prop2 == null ? string.CompareOrdinal(propName1, propName2) : 1; + } + + if (prop2 == null) + { + return -1; + } + + if (ReferenceEquals(prop1, prop2)) + { + return string.CompareOrdinal(propName1, propName2); + } + + if (prop1.IsParentOf(prop2)) + { + return 1; + } + + if (prop2.IsParentOf(prop1)) + { + return -1; + } + + return 0; + }); + + foreach (var (property, propertyName) in sortedNotifications) + { + if (property == null) + { + _model.OnPropertyChanged(propertyName); + } + else + { + property.OnPropertyChanged(propertyName); + } + } + + _notifications.Clear(); + } } } - public ModelProperty(string name, T value, Func validate) + private readonly Validator _validator; + public BillingProperty Root { get; } + + public Model(Billing billing) { - Name = name; - _value = value; - _errors = new List {validate?.Invoke(Name, _value) ?? ""}; - _validate = validate; - _modified = false; + _validator = new Validator(this); + + using var validator = BeginValidation(); + Root = new BillingProperty(this, null, billing); } - public override IReadOnlyCollection Errors => _errors[0] != "" ? _errors : EmptyList; - - public override bool Modified => _modified; - - public void ResetModified() + public Validator BeginValidation() { - _modified = false; + _validator.IncValidationCounter(); + return _validator; } - } - - public static class ModelPropertyValidators - { - public static string IsNotEmpty(string name, string data) - { - return string.IsNullOrEmpty(data) ? $"{name}: Geben Sie einen Wert ein." : ""; - } - - public static string IsNaturalInt(string name, string data) - { - return !int.TryParse(data, out var n) || n <= 0 - ? $"{name}: Der eingegebene Wert ist keine gültige positive Zahl > 0." - : ""; - } - } - - public abstract class Model : INotifyPropertyChanged - { - private readonly List _properties; - - protected Model() - { - _properties = new List(); - - PropertyChanged += (sender, args) => - { - if (args.PropertyName == "Errors") - { - OnPropertyChanged("ErrorCount"); - } - }; - } - - protected T Add(T property) where T : ModelBaseProperty - { - _properties.Add(property); - - property.PropertyChanged += (sender, args) => - { - if (args.PropertyName == "Errors" || args.PropertyName == "Modified") - { - OnPropertyChanged(args.PropertyName); - } - }; - - return property; - } - - public bool Modified => _properties.Select(property => property.Modified).Any(); - - public IReadOnlyCollection Errors - { - get - { - var errors = new List(); - - foreach (var property in _properties) - { - errors.AddRange(property.Errors); - } - - return errors; - } - } - - public int ErrorCount => Errors.Count; - - public abstract void Apply(); public event PropertyChangedEventHandler PropertyChanged; [NotifyPropertyChangedInvocator] - protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + private void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } diff --git a/ucalc/Models/Properties.cs b/ucalc/Models/Properties.cs new file mode 100644 index 0000000..fbea7c5 --- /dev/null +++ b/ucalc/Models/Properties.cs @@ -0,0 +1,326 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using UCalc.Annotations; + +namespace UCalc.Models +{ + public abstract class Property : INotifyPropertyChanged + { + protected static readonly IReadOnlyList EmptyList = new List(); + + protected readonly Model Model; + public Property Parent { get; } + + public abstract IReadOnlyList Errors { get; } + + private bool _modified; + + public bool Modified + { + get => _modified; + protected set + { + if (_modified == value) + { + return; + } + + _modified = value; + + using var validator = Model.BeginValidation(); + validator.Notify(this, "Modified"); + } + } + + protected Property(Model model, Property parent) + { + Model = model; + Parent = parent; + } + + public abstract void Validate(); + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public bool IsParentOf(Property property) + { + while (property != null) + { + if (ReferenceEquals(this, property)) + { + return true; + } + + property = property.Parent; + } + + return false; + } + } + + public abstract class ValueProperty : Property + { + protected string Name; + private T _value; + private readonly List _errors; + + public T Value + { + get => _value; + set + { + if (_value.Equals(value)) + { + return; + } + + _value = value; + OnPropertyChanged(); + + using var validator = Model.BeginValidation(); + Modified = true; + validator.Validate(this); + } + } + + protected ValueProperty(Model model, Property parent, string name, T value) : base(model, parent) + { + Name = name; + _value = value; + _errors = new List {""}; + + using var validator = Model.BeginValidation(); + validator.Validate(this); + } + + public override IReadOnlyList Errors => _errors[0] != "" ? _errors : EmptyList; + + public sealed override void Validate() + { + var oldError = _errors[0]; + _errors[0] = ValidateValue(); + + if (oldError != _errors[0]) + { + using var validator = Model.BeginValidation(); + validator.Notify(this, "Errors"); + } + } + + protected abstract string ValidateValue(); + } + + public class AlwaysValidProperty : ValueProperty + { + public AlwaysValidProperty(Model model, Property parent, string name, T value) : base(model, parent, + name, value) + { + } + + protected override string ValidateValue() + { + return ""; + } + } + + public class NotEmptyStringProperty : ValueProperty + { + public NotEmptyStringProperty(Model model, Property parent, string name, string value) : base(model, parent, + name, value) + { + } + + protected override string ValidateValue() + { + return string.IsNullOrEmpty(Value) ? $"{Name}: Geben Sie einen Wert ein." : ""; + } + } + + public class NaturalNumberProperty : ValueProperty + { + public NaturalNumberProperty(Model model, Property parent, string name, int value) : base(model, parent, name, + value.ToString()) + { + } + + protected override string ValidateValue() + { + return !int.TryParse(Value, out var n) || n <= 0 + ? $"{Name}: Der eingegebene Wert ist keine gültige Zahl > 0." + : ""; + } + } + + public class PositiveDecimalProperty : ValueProperty + { + public PositiveDecimalProperty(Model model, Property parent, string name, decimal value) : base(model, parent, + name, + value.ToString(Constants.DecimalFormat)) + { + } + + protected override string ValidateValue() + { + return !decimal.TryParse(Value, out var n) || n < 0 + ? $"{Name}: Der eingegebene Wert ist kein gültiger Betrag." + : ""; + } + } + + public abstract class NestedProperty : Property + { + private readonly List _properties; + private readonly List _errors; + public override IReadOnlyList Errors => _errors; + + protected NestedProperty(Model model, Property parent) : base(model, parent) + { + _properties = new List(); + _errors = new List(); + } + + protected T Add(T property) where T : Property + { + _properties.Add(property); + return property; + } + + public override void Validate() + { + using var validator = Model.BeginValidation(); + validator.ValidateRange(_properties); + } + + public override void OnPropertyChanged(string propertyName = null) + { + switch (propertyName) + { + case "Errors": + _errors.Clear(); + + foreach (var property in _properties) + { + _errors.AddRange(property.Errors); + } + + break; + case "Modified": + Modified = _properties.Any(property => property.Modified); + break; + } + + base.OnPropertyChanged(propertyName); + } + } + + public abstract class MultiProperty : Property, IReadOnlyList, INotifyCollectionChanged where T : Property + { + private readonly string _errorOnEmpty; + private readonly List _properties; + protected IReadOnlyList Properties => _properties; + private readonly List _errors; + public override IReadOnlyList Errors => _errors; + + protected MultiProperty(Model model, Property parent, string errorOnEmpty = null) : base(model, parent) + { + _errorOnEmpty = errorOnEmpty; + _properties = new List(); + _errors = new List(); + + OnPropertyChanged("Errors"); + } + + protected void Add(T property) + { + _properties.Add(property); + + using var validator = Model.BeginValidation(); + validator.Notify(this, "Errors"); + Modified = true; + + OnPropertyChanged("Count"); + + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property, + _properties.Count - 1)); + } + + protected void Remove(T property) + { + var index = _properties.IndexOf(property); + _properties.RemoveAt(index); + + using var validator = Model.BeginValidation(); + validator.Notify(this, "Errors"); + Modified = true; + + OnPropertyChanged("Count"); + + CollectionChanged?.Invoke(this, + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, property, index)); + } + + public override void Validate() + { + using var validator = Model.BeginValidation(); + validator.ValidateRange(_properties); + } + + public sealed override void OnPropertyChanged(string propertyName = null) + { + switch (propertyName) + { + case "Errors": + _errors.Clear(); + + if (_properties.Count == 0) + { + if (!string.IsNullOrEmpty(_errorOnEmpty)) + { + _errors.Add(_errorOnEmpty); + } + } + else + { + foreach (var property in _properties) + { + _errors.AddRange(property.Errors); + } + } + + break; + case "Modified": + Modified = Modified || _properties.Any(property => property.Modified); + break; + } + + base.OnPropertyChanged(propertyName); + } + + public IEnumerator GetEnumerator() + { + return _properties.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int Count => _properties.Count; + + public T this[int index] => _properties[index]; + + public event NotifyCollectionChangedEventHandler CollectionChanged; + } +} \ No newline at end of file diff --git a/ucalc/Models/SalutationProperty.cs b/ucalc/Models/SalutationProperty.cs new file mode 100644 index 0000000..d4ec12a --- /dev/null +++ b/ucalc/Models/SalutationProperty.cs @@ -0,0 +1,12 @@ +using UCalc.Data; + +namespace UCalc.Models +{ + public class SalutationProperty : AlwaysValidProperty + { + public SalutationProperty(Model model, Property parent, string name, Salutation value) : base(model, parent, + name, (int) value) + { + } + } +} \ No newline at end of file diff --git a/ucalc/Models/TenantProperty.cs b/ucalc/Models/TenantProperty.cs new file mode 100644 index 0000000..1624e6a --- /dev/null +++ b/ucalc/Models/TenantProperty.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using UCalc.Controls; +using UCalc.Data; + +namespace UCalc.Models +{ + public class EntryDateProperty : ValueProperty + { + public EntryDateProperty(Model model, Property parent, string name, DateTime? value) : base(model, parent, name, + value) + { + } + + protected override string ValidateValue() + { + using var validator = Model.BeginValidation(); + + validator.Validate(((TenantProperty) Parent).DepartureDate); + + return ""; + } + } + + public class DepatureDateProperty : ValueProperty + { + public DepatureDateProperty(Model model, Property parent, string name, DateTime? value) : base(model, parent, + name, value) + { + } + + protected override string ValidateValue() + { + if (Value != null) + { + var entryDate = ((TenantProperty) Parent).EntryDate.Value; + + if (entryDate != null && entryDate > Value) + { + return $"{Name}: Das Datum liegt vor dem Einzug."; + } + } + + using var validator = Model.BeginValidation(); + validator.Validate(((TenantProperty) Parent).RentedFlats); + + return ""; + } + } + + public class RentedFlatsProperty : Property + { + private const string NoFlatsError = "Gemietete Wohnungen: Es wurde keine Wohnung zugewiesen."; + private readonly HashSet _flatProperties; + private readonly List _errors; + + public RentedFlatsProperty(Model model, Property parent, IEnumerable data, + IReadOnlyDictionary flatToProperty = null) : base(model, parent) + { + _flatProperties = new HashSet(); + _errors = new List(); + + foreach (var flat in data) + { + _flatProperties.Add(flatToProperty?[flat] ?? throw new InvalidOperationException()); + } + + Validate(); + } + + public override IReadOnlyList Errors => _errors; + + public void Add(FlatProperty flat) + { + if (!_flatProperties.Add(flat)) + { + return; + } + + using var validator = Model.BeginValidation(); + validator.Validate(this); + Modified = true; + } + + public void Remove(FlatProperty flat) + { + if (!_flatProperties.Remove(flat)) + { + return; + } + + using var validator = Model.BeginValidation(); + validator.Validate(this); + Modified = true; + } + + public bool Contains(FlatProperty flat) + { + return _flatProperties.Contains(flat); + } + + public sealed override void Validate() + { + _errors.Clear(); + + if (_flatProperties.Count == 0) + { + _errors.Add(NoFlatsError); + return; + } + + var entryDate = ((TenantProperty) Parent).EntryDate.Value; + var departureDate = ((TenantProperty) Parent).DepartureDate.Value; + + foreach (var flatProperty in _flatProperties) + { + foreach (var tenant in Model.Root.Tenants) + { + if (ReferenceEquals(this, tenant.RentedFlats)) + { + continue; + } + + if (tenant.RentedFlats.IsRented(flatProperty, entryDate, departureDate)) + { + var error = $"Gemietete Wohnungen: Die Wohnung \"{flatProperty.Name.Value}\" ist bereits "; + + if (entryDate != null) + { + if (departureDate != null) + { + _errors.Add( + $"von {entryDate.Value.ToString(Constants.DateFormat)} - {departureDate.Value.ToString(Constants.DateFormat)} "); + } + else + { + _errors.Add($"seit dem {entryDate.Value.ToString(Constants.DateFormat)} "); + } + } + else if (departureDate != null) + { + _errors.Add($"bis zum {departureDate.Value.ToString(Constants.DateFormat)} "); + } + + error += "an einen anderen Mieter vermietet."; + _errors.Add(error); + break; + } + } + } + + using var validator = Model.BeginValidation(); + validator.Notify(this, "Errors"); + + validator.ValidateRange(Model.Root.Tenants); + } + + private bool IsRented(FlatProperty flat, DateTime? start, DateTime? end) + { + if (!_flatProperties.Contains(flat)) + { + return false; + } + + var thisStart = ((TenantProperty) Parent).EntryDate.Value ?? DateTime.MinValue; + var thisEnd = ((TenantProperty) Parent).DepartureDate.Value ?? DateTime.MaxValue; + + var otherStart = start ?? DateTime.MinValue; + var otherEnd = end ?? DateTime.MinValue; + + return thisStart.Intersects(thisEnd, otherStart, otherEnd); + } + } + + public class TenantProperty : NestedProperty + { + public SalutationProperty Salutation { get; } + public NotEmptyStringProperty Name { get; } + public NaturalNumberProperty PersonCount { get; } + public BankAccountProperty BankAccount { get; } + public EntryDateProperty EntryDate { get; } + public DepatureDateProperty DepartureDate { get; } + public RentedFlatsProperty RentedFlats { get; } + public PositiveDecimalProperty PaidRent { get; } + public AlwaysValidProperty CustomMessage1 { get; } + public AlwaysValidProperty CustomMessage2 { get; } + + public TenantProperty(Model model, Property parent, Tenant data, + IReadOnlyDictionary flatToProperty = null) : base(model, parent) + { + Salutation = Add(new SalutationProperty(model, this, "Anrede", data.Salutation)); + Name = Add(new NotEmptyStringProperty(model, this, "Name", data.Name)); + PersonCount = Add(new NaturalNumberProperty(model, this, "Personenanzahl", data.PersonCount)); + BankAccount = Add(new BankAccountProperty(model, this, data.BankAccount)); + EntryDate = Add(new EntryDateProperty(model, this, "Einzugsdatum", data.EntryDate)); + DepartureDate = Add(new DepatureDateProperty(model, this, "Auszugsdatum", data.DepartureDate)); + RentedFlats = Add(new RentedFlatsProperty(model, this, data.RentedFlats, flatToProperty)); + PaidRent = Add(new PositiveDecimalProperty(model, this, "Bereits gezahlt", data.PaidRent)); + CustomMessage1 = Add(new AlwaysValidProperty(model, this, "Nachricht 1", data.CustomMessage1)); + CustomMessage2 = Add(new AlwaysValidProperty(model, this, "Nachricht 2", data.CustomMessage2)); + } + } +} \ No newline at end of file diff --git a/ucalc/Models/TenantsProperty.cs b/ucalc/Models/TenantsProperty.cs new file mode 100644 index 0000000..fd8b33a --- /dev/null +++ b/ucalc/Models/TenantsProperty.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.Linq; +using UCalc.Data; + +namespace UCalc.Models +{ + public class TenantsProperty : MultiProperty + { + public TenantsProperty(Model model, Property parent, IEnumerable data, + IReadOnlyDictionary flatToProperty = null) : base(model, parent, + "Mieter: Geben Sie einen oder mehr Mieter an.") + { + using var validator = Model.BeginValidation(); + + foreach (var tenant in data) + { + base.Add(new TenantProperty(Model, this, tenant, flatToProperty)); + } + + Modified = false; + } + + public void Add() + { + using var validator = Model.BeginValidation(); + + base.Add(new TenantProperty(Model, this, new Tenant())); + } + + public new void Remove(TenantProperty tenant) + { + using var validator = Model.BeginValidation(); + + base.Remove(tenant); + + validator.ValidateRange(Properties.Select(otherTenant => otherTenant.RentedFlats)); + } + } +} \ No newline at end of file diff --git a/ucalc/NewWindow.xaml.cs b/ucalc/NewWindow.xaml.cs index 6eca657..6b684c5 100644 --- a/ucalc/NewWindow.xaml.cs +++ b/ucalc/NewWindow.xaml.cs @@ -71,7 +71,7 @@ namespace UCalc return; } - // Remove depatured renters and costs that were only paid once + // Remove departured renters and costs that were only paid once string summary = ""; billing.StartDate = (DateTime) StartCalendar.SelectedDate; billing.EndDate = (DateTime) EndCalendar.SelectedDate; diff --git a/ucalc/Pages/HousePage.xaml b/ucalc/Pages/HousePage.xaml index 685c7d0..506b90b 100644 --- a/ucalc/Pages/HousePage.xaml +++ b/ucalc/Pages/HousePage.xaml @@ -8,6 +8,10 @@ xmlns:controls="clr-namespace:UCalc.Controls" mc:Ignorable="d"> + + + + @@ -22,12 +26,12 @@ + Property="{Binding Path=House.Address.Street, RelativeSource={RelativeSource AncestorType=pages:HousePage}}" /> + Text="{Binding Path=House.Address.Street.Value, RelativeSource={RelativeSource AncestorType=pages:HousePage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -39,12 +43,12 @@ + Property="{Binding Path=House.Address.HouseNumber, RelativeSource={RelativeSource AncestorType=pages:HousePage}}" /> + Text="{Binding Path=House.Address.HouseNumber.Value, RelativeSource={RelativeSource AncestorType=pages:HousePage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -56,12 +60,12 @@ + Property="{Binding Path=House.Address.City, RelativeSource={RelativeSource AncestorType=pages:HousePage}}" /> + Text="{Binding Path=House.Address.City.Value, RelativeSource={RelativeSource AncestorType=pages:HousePage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -73,45 +77,53 @@ + Property="{Binding Path=House.Address.Postcode, RelativeSource={RelativeSource AncestorType=pages:HousePage}}" /> + Text="{Binding Path=House.Address.Postcode.Value, RelativeSource={RelativeSource AncestorType=pages:HousePage}, UpdateSourceTrigger=PropertyChanged}" /> - - - - - - - - - - - - - + + - + + + + + + + + + + + + + + + + @@ -143,7 +155,7 @@ HighlightBackground="{x:Static local:Constants.MainColor}" Click="OnFlatEditClick"> - diff --git a/ucalc/Pages/HousePage.xaml.cs b/ucalc/Pages/HousePage.xaml.cs index 026de8b..658ecf4 100644 --- a/ucalc/Pages/HousePage.xaml.cs +++ b/ucalc/Pages/HousePage.xaml.cs @@ -7,7 +7,7 @@ namespace UCalc.Pages public partial class HousePage { public Window ParentWindow { get; set; } - public HouseModel Model { get; set; } + public HouseProperty House { get; set; } public HousePage() { @@ -16,25 +16,25 @@ namespace UCalc.Pages private void OnAddFlatClick(object sender, RoutedEventArgs e) { - Model.AddFlat(); + House.Flats.Add(); } private void OnFlatDeleteClick(object sender, RoutedEventArgs e) { - var flatModel = (FlatModel) ((HighlightButton) sender).DataContext; + var flat = (FlatProperty) ((HighlightButton) sender).DataContext; - if (MessageBox.Show($"Möchten Sie die Wohnung \"{flatModel.Data.Name}\" wirlick löschen?", "Löschen?", + if (MessageBox.Show($"Möchten Sie die Wohnung \"{flat.Name.Value}\" wirlick löschen?", "Löschen?", MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) { - Model.RemoveFlat(flatModel); + House.Flats.Remove(flat); } } private void OnFlatEditClick(object sender, RoutedEventArgs e) { - var flatModel = (FlatModel) ((HighlightButton) sender).DataContext; + var flat = (FlatProperty) ((HighlightButton) sender).DataContext; - new FlatWindow(flatModel) {Owner = ParentWindow}.ShowDialog(); + new FlatWindow(flat) {Owner = ParentWindow}.ShowDialog(); } } } \ No newline at end of file diff --git a/ucalc/Pages/LandlordPage.xaml b/ucalc/Pages/LandlordPage.xaml index 8033dea..952530e 100644 --- a/ucalc/Pages/LandlordPage.xaml +++ b/ucalc/Pages/LandlordPage.xaml @@ -21,7 +21,7 @@ + SelectedIndex="{Binding Path=Landlord.Salutation.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}"/> @@ -33,12 +33,12 @@ + Property="{Binding Path=Landlord.Name, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.Name.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -50,12 +50,12 @@ + Property="{Binding Path=Landlord.Phone, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.Phone.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -67,12 +67,12 @@ + Property="{Binding Path=Landlord.MailAddress, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.MailAddress.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> + Property="{Binding Path=Landlord.Address.Street, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.Address.Street.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -104,12 +104,12 @@ + Property="{Binding Path=Landlord.Address.HouseNumber, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.Address.HouseNumber.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -121,12 +121,12 @@ + Property="{Binding Path=Landlord.Address.City, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.Address.City.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -138,12 +138,12 @@ + Property="{Binding Path=Landlord.Address.Postcode, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.Address.Postcode.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> + Property="{Binding Path=Landlord.BankAccount.Iban, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.BankAccount.Iban.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -175,12 +175,12 @@ + Property="{Binding Path=Landlord.BankAccount.Bic, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.BankAccount.Bic.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> @@ -192,12 +192,12 @@ + Property="{Binding Path=Landlord.BankAccount.BankName, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}}" /> + Text="{Binding Path=Landlord.BankAccount.BankName.Value, RelativeSource={RelativeSource AncestorType=pages:LandlordPage}, UpdateSourceTrigger=PropertyChanged}" /> diff --git a/ucalc/Pages/LandlordPage.xaml.cs b/ucalc/Pages/LandlordPage.xaml.cs index 1432500..b5817ba 100644 --- a/ucalc/Pages/LandlordPage.xaml.cs +++ b/ucalc/Pages/LandlordPage.xaml.cs @@ -4,7 +4,7 @@ namespace UCalc.Pages { public partial class LandlordPage { - public LandlordModel Model { get; set; } + public LandlordProperty Landlord { get; set; } public LandlordPage() { diff --git a/ucalc/Pages/SideBar.xaml b/ucalc/Pages/SideBar.xaml index f78380e..7b5e1f5 100644 --- a/ucalc/Pages/SideBar.xaml +++ b/ucalc/Pages/SideBar.xaml @@ -119,7 +119,7 @@ + Property="{Binding Path=Model.Root.Landlord, RelativeSource={RelativeSource AncestorType=pages:SideBar}}"/>