C
C#2mo 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?
8 Replies
Sir Rufo
Sir Rufo2mo 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
Zarez2mo 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 Rufo2mo ago
Sorry, I have no clue why you are sending this to me
Zarez
Zarez2mo ago
Just extending your message by docs
Zoli
ZoliOP2mo 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();
}
blinkbat
blinkbat2mo ago
pick one for storage and use it consistently, let's say kg. ensure everything going into the db is kg and everything displayed in the app takes that into account and converts it on the view layer according to the user's state. not really a framework specific question, this applies to everything -- data should be consistent, views may vary. this allows you to add more variance to views, or reuse the data in other apps or services, etc. also, to answer your Q, you are not changing the data this way, only how it's presented. ensure you only work with the actual data (kg if that's your choice) in biz logic, don't replace the value on the frontend and then re-replace it when sending it back in, etc. (which is to say, again, only change how it's represented, and only do so in the view) as in the view is literally <span>@ConvertWeight(actualWeightInKg, userPreference)</span>, you know?
Zoli
ZoliOP2mo ago
Thanks a lot, I already used controllers across the application to display the correct weight with Labels. I got kind of confused with Entry input.That I have solved with viewmodel but most likely I will move to use controller. I created a behaviour UnitAwareWeightBehavior makes the Entry always display the user’s preferred unit (kg or lbs) while transparently converting any typed value back into kilograms, so my ViewModel property always stays in a consistent unit.
Sir Rufo
Sir Rufo2mo ago
I guess you mean "converter" when you say "controller" 😁

Did you find this page helpful?