C
C#14h ago
Zoli

View or ViewModel duty?

I am building a Maui application where the user can switch between using "kg" or "lbs" across the application. So the main dilemma I have a Weight property and how to update if the preferred unit has been changed by the user the two ways to update: Some extra information I store the weight in kg in the database there are many collectionviews, also tabbed view so singleton View with unit converter For this I could just use MultiBinding to pass the currenct weight value and if currently UseImperialUnits state, ApplicationState is added to the App constructor as resource.
<Label>
<Label.FormattedText>
<FormattedString>
<Span>
<Span.Text>
<MultiBinding Converter="{StaticResource WeightWithUnitConverter}">
<Binding Path="Weight" />
<Binding Path="UseImperialUnits" Source="{StaticResource ApplicationState}" />
</MultiBinding>
</Span.Text>
</Span>
</FormattedString>
</Label.FormattedText>
</Label>
<Label>
<Label.FormattedText>
<FormattedString>
<Span>
<Span.Text>
<MultiBinding Converter="{StaticResource WeightWithUnitConverter}">
<Binding Path="Weight" />
<Binding Path="UseImperialUnits" Source="{StaticResource ApplicationState}" />
</MultiBinding>
</Span.Text>
</Span>
</FormattedString>
</Label.FormattedText>
</Label>
This works so if user switchs the weight unit it updates the ui automatically and calculates the new value for only displaying underhood the Weight property is not changed. ViewModel I have a getter property on the ViewModel: public double Weight => _applicationState.UseImperialUnits ? _model.Weight.ToPounds() : _model.Weight; In this case for sure I would need to subscribe to the PropertyChange of the UseImperialUnits.ApplicationState so if that changes we need to call: OnPropertyChanged(nameof(Weight )) this means in collection I would need to do with all items. Which approach do you consider best practice in MVVM for handling global unit system preferences? Do you stick with converters for UI-only scenarios, or do you prefer propagating PropertyChanged through all affected VMs? Any recommended patterns to make this scalable and avoid memory leaks?
5 Replies
Sir Rufo
Sir Rufo11h ago
It is a presentation thing so leave it at the presentation layer. It is same as datetime values. Store them in UTC but when showing to the users (presentation) then convert them to local time. When you move on and want to support even more Weight units only change the presentation. The logic will be untouched
Zarez
Zarez9h ago
Binding value converters - .NET MAUI
Learn how how to cast or convert values within a .NET MAUI data binding by implementing a value converter (which is also known as a binding converter, or binding value converter).
Sir Rufo
Sir Rufo8h ago
Sorry, I have no clue why you are sending this to me
Zarez
Zarez8h ago
Just extending your message by docs
Zoli
ZoliOP4h ago
@Sir Rufo I would have one more question if you dont mind. What about Entry? Right now I am using reactive way of viewmodel so when the binded property changed by the entry I am updating the Weight of the model and convert the input if its in lbs to store in kg. Should this also be solved by controller? So I could use a controller and convert the value to store the equivalent in kg? What do you think about this? You can see the code below.
private double? _weight;

partial void OnWeightChanged(double? value)
{
if (_applicationStateService.UseImperialUnits)
{
// Weight is in pounds, convert to kg because model stores weight in kg
if (value.HasValue && value > 0)
{
_model.Weight = Math.Round(Mass.FromPounds(value.Value).Kilograms, 2, MidpointRounding.AwayFromZero);
}
else
{
_model.Weight = 0;
}
}
else
{
_model.Weight = value.EnsurePositiveOrZero();
}
}
private double? _weight;

partial void OnWeightChanged(double? value)
{
if (_applicationStateService.UseImperialUnits)
{
// Weight is in pounds, convert to kg because model stores weight in kg
if (value.HasValue && value > 0)
{
_model.Weight = Math.Round(Mass.FromPounds(value.Value).Kilograms, 2, MidpointRounding.AwayFromZero);
}
else
{
_model.Weight = 0;
}
}
else
{
_model.Weight = value.EnsurePositiveOrZero();
}
}
I have already the controller ready for Label, i post here maybe its useful for others:
public sealed class WeightWithUnitConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values is null || values.Length < 2) return string.Empty;
if (values[0] is not IConvertible || values[1] is not bool useImperial) return string.Empty;

double kg = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture);

// Convert with UnitsNet
double displayWeight = useImperial
? Mass.FromKilograms(kg).Pounds
: kg;

// Round to 2 decimals, away from zero (consistent with your other code)
displayWeight = Math.Round(displayWeight, 2, MidpointRounding.AwayFromZero);

// Show no decimals if whole number, otherwise 2 decimals
string formatted = Math.Abs(displayWeight % 1) < 0.000001
? displayWeight.ToString("0", culture)
: displayWeight.ToString("0.00", culture);

// Use simple unit labels; or see the localized option below
string unit = useImperial ? AppResources.PoundUnitShort : AppResources.KilogramUnitShort;

return $"{formatted} {unit}";
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
public sealed class WeightWithUnitConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values is null || values.Length < 2) return string.Empty;
if (values[0] is not IConvertible || values[1] is not bool useImperial) return string.Empty;

double kg = System.Convert.ToDouble(values[0], CultureInfo.InvariantCulture);

// Convert with UnitsNet
double displayWeight = useImperial
? Mass.FromKilograms(kg).Pounds
: kg;

// Round to 2 decimals, away from zero (consistent with your other code)
displayWeight = Math.Round(displayWeight, 2, MidpointRounding.AwayFromZero);

// Show no decimals if whole number, otherwise 2 decimals
string formatted = Math.Abs(displayWeight % 1) < 0.000001
? displayWeight.ToString("0", culture)
: displayWeight.ToString("0.00", culture);

// Use simple unit labels; or see the localized option below
string unit = useImperial ? AppResources.PoundUnitShort : AppResources.KilogramUnitShort;

return $"{formatted} {unit}";
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}

Did you find this page helpful?