Skip to content

WPF MVVM Sample App #353

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 7, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Samples/WpfMVVMSample/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## WPF MVVM Sample
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This readme is great!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool

This is a simple sample showing how UnitsNet can be used to create a WPF MVVM application. I have used this strategy in a few simple engineering apps and thought I would share it as a sample to see if others might offer improvements.

It performs a simple calculation allowing flexibility in the units for parameters and results. Default units for each are specified in the settings drop down.

A key feature enabling this sample is the [UnitToStringConverter](https://github.com/dayewah/UnitsNet/blob/master/Samples/WpfMVVMSample/WpfMVVMSample/Converters/UnitToStringConverter.cs) class
- If a parameter is entered as a number the unit is assigned automatically
- If a parameter is entered as a unit other than the default it is converted automatically
- If a non-compatible unit is used a validation error is triggered

The default unit for each parameter and the result can be changed from the settings pull down.

The number of significant digits displayed can also be changed from settings.
25 changes: 25 additions & 0 deletions Samples/WpfMVVMSample/WpfMVVMSample.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2010
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfMVVMSample", "WpfMVVMSample\WpfMVVMSample.csproj", "{B72F9215-70FF-4155-89BC-9A02CC550447}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B72F9215-70FF-4155-89BC-9A02CC550447}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B72F9215-70FF-4155-89BC-9A02CC550447}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B72F9215-70FF-4155-89BC-9A02CC550447}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B72F9215-70FF-4155-89BC-9A02CC550447}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D5FA6C7A-26EC-4F45-B539-F20AD40CE0A4}
EndGlobalSection
EndGlobal
6 changes: 6 additions & 0 deletions Samples/WpfMVVMSample/WpfMVVMSample/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
</configuration>
8 changes: 8 additions & 0 deletions Samples/WpfMVVMSample/WpfMVVMSample/App.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Application x:Class="WpfMVVMSample.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfMVVMSample">
<Application.Resources>

</Application.Resources>
</Application>
24 changes: 24 additions & 0 deletions Samples/WpfMVVMSample/WpfMVVMSample/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;

namespace WpfMVVMSample
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);

var bootstrapper = new Bootstrapper();
bootstrapper.Run();
}
}
}
43 changes: 43 additions & 0 deletions Samples/WpfMVVMSample/WpfMVVMSample/Bootstrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Microsoft.Practices.ServiceLocation;
using Microsoft.Practices.Unity;
using Prism.Unity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using WpfMVVMSample.Converters;
using WpfMVVMSample.Settings;

namespace WpfMVVMSample
{
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return Container.TryResolve<MainWindow>();
}

protected override void InitializeShell()
{
Application.Current.MainWindow.Show();
}

protected override void ConfigureContainer()
{
base.ConfigureContainer();

Container.RegisterType<SettingsManager>(new ContainerControlledLifetimeManager());//singleton
}

protected override void ConfigureServiceLocator()
{
base.ConfigureServiceLocator();

var serviceLocator = new UnityServiceLocator(Container);
ServiceLocator.SetLocatorProvider(() => serviceLocator);

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Markup;

namespace WpfMVVMSample.Converters
{
public class EnumBindingSourceExtension : MarkupExtension
{
private Type _enumType;
private Type EnumType
{
get { return this._enumType; }
set
{
if (value != this._enumType)
{
if (value != null)
{
Type enumType = Nullable.GetUnderlyingType(value) ?? value;

if (!enumType.IsEnum)
throw new ArgumentException("Type must be for an Enum.");
}

this._enumType = value;
}
}
}

public EnumBindingSourceExtension() { }

public EnumBindingSourceExtension(Type enumType)
{
this.EnumType = enumType;
}

public override object ProvideValue(IServiceProvider serviceProvider)
{
if (this._enumType== null)
throw new InvalidOperationException("The EnumType must be specified.");

Type actualEnumType = Nullable.GetUnderlyingType(this._enumType) ?? this._enumType;

//omits the first enum element, typically "undefined"
var enumValues = Enum.GetValues(actualEnumType).Cast<object>().Skip(1);

return enumValues;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using Microsoft.Practices.ServiceLocation;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
using UnitsNet;
using WpfMVVMSample.Settings;

namespace WpfMVVMSample.Converters
{
public class UnitToStringConverter :MarkupExtension, IValueConverter
{
//http://www.thejoyofcode.com/WPF_Quick_Tip_Converters_as_MarkupExtensions.aspx
private SettingsManager _settings;
private static UnitToStringConverter _instance;

public UnitToStringConverter()
{
if (!DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
_settings = ServiceLocator.Current.GetInstance<SettingsManager>();
}
}


public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var unitType = value.GetType();
var unitEnumType = unitType.GetProperty("BaseUnit").PropertyType;
var unitEnumValue = _settings.GetDefaultUnit(unitEnumType);
var significantDigits = _settings.SignificantDigits;

var result = unitType
.GetMethod("ToString", new[] { unitEnumType, typeof(IFormatProvider), typeof(int) })
.Invoke(value, new object[] { unitEnumValue, null, significantDigits });
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My only concern is that this is quite heavy on reflection and might easily break at runtime when subtle changes to the library are made that are backwards compatible on calling code, but incompatible with the reflection method signatures expected here.

I think maybe the library could be adapted a bit to facilitate this better. I have only given this a brief thinking, but I I think it would help to make https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/CustomCode/QuantityParser.cs public in order to give access to the quantity value+unit parsing without having to use all the reflection bits?
I would need to spend more time evaluating this, but I think we should be able to simplify this type of integration to avoid so much reflection code. For now just consider this a rambling comment as I don't have a clear vision for how to best achieve that yet.

@JKSnd @eriove @ferittuncer I'm seeing some potential usecases for exposing BaseUnit and ToString overloads in an interface or base class for quantities here, as discussed earlier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that the reflection is a bit clunky. Any library enhancements that better enable this strategy would be welcome.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we have experienced these problems too when we used this library in our GUI. Lacking base classes for units and quantities forces implementation to reflection as presenter layer doesn't know which unit or quantity it's dealing with at the runtime.

Perhaps there are other ways we couldn't see, I'm not sure. I just have an instinct that we should have base classes. You know better, your call @angularsen

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A common interface for all quantities would be a simple addition (at least if it contained members that are already there) and wouldn't incur any performance hit (boxing) as long as it isn't used.

That's of course not part of this pull request but it is much easier to see in this context.


return result;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var unitEnumType = targetType.GetProperty("BaseUnit").PropertyType;
var unitEnumValue = _settings.GetDefaultUnit(unitEnumType);

if ((string)value == "") return 0.0;

double number;
if (double.TryParse((string)value, out number))
return ParseDouble(targetType, number, unitEnumType, unitEnumValue);

try
{
return ParseUnit(value, targetType);
}
catch (Exception e)
{
return new ValidationResult(false, e.InnerException.Message);
}
}


private static object ParseDouble(Type targetType, double number, Type unitEnumType, object unitEnumValue)
{
return targetType
.GetMethod("From", new[] { typeof(QuantityValue), unitEnumType })
.Invoke(null, new object[] { (QuantityValue)number, unitEnumValue });
}
private static object ParseUnit(object value, Type targetType)
{
return targetType
.GetMethod("Parse", new[] { typeof(string) })
.Invoke(null, new object[] { value });
}

public override object ProvideValue(IServiceProvider serviceProvider)
{
return _instance ?? (_instance = new UnitToStringConverter());
}
}
}
111 changes: 111 additions & 0 deletions Samples/WpfMVVMSample/WpfMVVMSample/MainWindow.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<Window x:Class="WpfMVVMSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/"
xmlns:conv="clr-namespace:WpfMVVMSample.Converters"
xmlns:local="clr-namespace:WpfMVVMSample"
xmlns:units="clr-namespace:UnitsNet.Units;assembly=UnitsNet"
prism:ViewModelLocator.AutoWireViewModel="True"

mc:Ignorable="d"
Title="MainWindow">
<Window.Resources>
<Style TargetType="TextBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Height" Value="30"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="10"/>
</Style>
<Style TargetType="ComboBox">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Height" Value="30"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Margin" Value="10"/>
</Style>
<Style TargetType="Label" x:Key="ResultLabel">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Height" Value="30"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Width" Value="150"/>
<Setter Property="Margin" Value="10"/>
</Style>
<Style TargetType="Label">
<Setter Property="HorizontalAlignment" Value="Right"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Margin" Value="10,0"/>
<Setter Property="FontSize" Value="16"/>
</Style>
</Window.Resources>
<StackPanel>
<Expander Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="3" FontSize="16" Background="WhiteSmoke">
<Expander.Header>
<Border>
<Label Content="Settings"></Label>
</Border>
</Expander.Header>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<Label Grid.Column="0" Grid.Row="0" Content="Mass"/>
<ComboBox Grid.Column="1"
ItemsSource="{Binding Source={conv:EnumBindingSource {x:Type units:MassUnit}}}"
SelectedItem="{Binding Settings.DefaultMassUnit}"/>

<Label Grid.Column="0" Grid.Row="1" Content="G" />
<ComboBox Grid.Column="1" Grid.Row="1"
ItemsSource="{Binding Source={conv:EnumBindingSource {x:Type units:AccelerationUnit}}}"
SelectedItem="{Binding Settings.DefaultAccelerationUnit}"/>

<Label Grid.Column="0" Grid.Row="2" Content="Weight" />
<ComboBox Grid.Column="1" Grid.Row="2"
ItemsSource="{Binding Source={conv:EnumBindingSource {x:Type units:ForceUnit}}}"
SelectedItem="{Binding Settings.DefaultForceUnit}"/>

<Label Grid.Column="0" Grid.Row="3" Content="No. Digits" />
<TextBox Grid.Column="1" Grid.Row="3" Text="{Binding Settings.SignificantDigits}"/>
</Grid>
</Expander>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<Label Grid.Column="0" Grid.Row="0" Content="Mass"/>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding ObjectMass, Converter={conv:UnitToStringConverter}}"/>

<Label Grid.Column="0" Grid.Row="1" Content="G" />
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding G, Converter={conv:UnitToStringConverter}}"/>

<Label Grid.Column="0" Grid.Row="2" Content="Weight" />
<Label Grid.Column="1" Grid.Row="2" Content="{Binding Weight, Converter={conv:UnitToStringConverter}}" Style="{StaticResource ResultLabel}"/>

</Grid>
</StackPanel>
</Window>
Loading