Perennialista
Perennialista

Reputation: 1302

How to transform information from ViewModel to show in the View

I am dealing with the apparently simple problem of showing the level of liquid in a tank using WPF and MVVM.

To show a tank I have used a DockPanel with a Rectangle inside. The height of the rectangle changes based of the quantity of liquid in the tank. At the top of the tank I have a TextBlock that shows the quantity of liquid present in the Tank. I have defined this in XAML like this:

<DockPanel x:Name="tankView" HorizontalAlignment="Left" Height="212" VerticalAlignment="Top" Width="144" DataContext="{Binding Source={StaticResource TankViewModel}}">
            <TextBlock x:Name="oilQuantity" HorizontalAlignment="Right" VerticalAlignment="Top" DockPanel.Dock="Top" Margin="0,0,10,0" Text = "{Binding TxtOilQuantity, Mode=OneWay}"/>
            <Rectangle x:Name="oilLevel" Fill="Green" Height="66" Stroke="Black" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" DockPanel.Dock="Bottom"/>
</DockPanel>

You can also see that I have created a TankViewModel class and a TankModel class, following the MVVM pattern.

Showing the quantity of liquid in the TextBlock is simple and a data binding does the job perfectly. However, when it comes to the Height of the rectangle some problems arise since I cannot find a way to properly separate the concerns between the View and the ViewModel.

The height of the Rectangle depends on the maximum capacity and on the quantity of liquid present in the tank, this way I could get a number that tells me in what percentage the tank is filled, like this:

public class TankViewModel : INotifyPropertyChanged
{
    private TankModel tankModel = new TankModel(2500);

    public int IntFilledPercentage {
        get {
            if (tankModel.OilQuantity == 0)
                return 0;
            else
                return Convert.ToInt32(((double)tankModel.OilQuantity / tankModel.capacity) * 100);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(String info) {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }
}

However, I cannot bind this property directly to the height of the rectangle and giving a value to such a property sounds like something the View should be in charge. To achieve this, I would have to insert some code in the View that translates this percentage value into the height of the rectangle.

Could I achieve this by implementing OnPropertyChanged() callback of the View?

Do you have any suggestions as to how to simplify the architecture I have put in place?

Upvotes: 3

Views: 356

Answers (3)

Dbuggy
Dbuggy

Reputation: 911

To complement rory. When you don't want a fixed Converter parameter but use the actual height from some container, it is also possible to use a MultiBinding and IMultiValueConverter.

This allows the height of the liquid to be set using the actual height of the parent.

In my example i'm using a Grid with borders to represent the liquid tank on the gui.

ViewModel:

public class MainWindowViewModel : PropertyChangedBase // from Calburn.Micro (see nuget)
{
    private int _liquidPerc;

    public MainWindowViewModel()
    {
        LiquidPercentage = 25;
    }

    public int LiquidPercentage
    {
        get { return _liquidPerc; }
        set
        {
            if (value == _liquidPerc) return;
            _liquidPerc= value;
            NotifyOfPropertyChange(() => LiquidPercentage);
        }
    }
}

Converter:

/// <summary>
/// Converter which expects two params. percentage and maximum height
/// </summary>
public class LiquidLevelConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var percentage = (int) values[0];
        var maxHeight = (double) values[1];
        return percentage*maxHeight*0.01;
    }

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

XAML:

<Window x:Class="UiLiquedTankDemo.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:local="clr-namespace:UiLiquedTankDemo"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:MainWindowViewModel x:Key="ViewModel" />
        <local:LiquidLevelConverter x:Key="LiquidLevelConverter" />
    </Window.Resources>
    <DockPanel DataContext="{StaticResource ViewModel}">

        <!-- move the slider to move the level of the liquid --> 
        <Slider Minimum="0" Maximum="100" Value="{Binding LiquidPercentage}" 
                DockPanel.Dock="Bottom" 
                Margin="0"/>

        <!-- Liquid container representation using a grid --> 
        <Grid Name="LiquidContainer" Margin="200,5">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Border Grid.Row="1" Background="Blue" Margin="0">
                <Border.Height>
                    <MultiBinding Converter="{StaticResource LiquidLevelConverter}">
                        <Binding Path="LiquidPercentage"></Binding>
                        <Binding Path="ActualHeight" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType={x:Type Grid}}"></Binding>
                    </MultiBinding>
                </Border.Height>
            </Border>

            <Border Grid.Row="0" Grid.RowSpan="2"  BorderBrush="Black" BorderThickness="1" />
        </Grid>

    </DockPanel>
</Window>

Upvotes: 2

rory.ap
rory.ap

Reputation: 35318

You can easily accomplish this using the percentage value from the view model by using a little math. You know the max height of the rectangle in the view (it's a static value probably). Then current height = max height multiplied by the percentage value.

Doing operations like this in XAML can be done using the Binding.Converter property with an IValueConverter. See this post, which is relevant.

Here's an example converter:

internal sealed class OilLevelConverter : System.Windows.Data.IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        var percentage = (decimal) value;
        var maxLevel = System.Convert.ToInt32((string) parameter);
        var currentLevel = System.Convert.ToInt32(maxLevel * percentage);
        return currentLevel;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Here's your App.xaml:

<Application x:Class="WpfApplication1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:WpfApplication1"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <local:ViewModel x:Key="ViewModel" />
        <local:OilLevelConverter x:Key="OilLevelConverter"/>
    </Application.Resources>
</Application>

And here's an example window XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" 
        Width="525" 
        DataContext="{StaticResource ViewModel}" >

    <Grid>
        <Rectangle Fill="#FFF4F4F5" 
                   HorizontalAlignment="Left" 
                   Height="{Binding Path=OilLevel, 
                    Converter={StaticResource OilLevelConverter}, 
                    ConverterParameter=100}" Margin="183,132,0,0" 
                    Stroke="Black" VerticalAlignment="Top" Width="100"/>
    </Grid>
</Window>

Note: I've left out the ViewModel which only has one property: OilLevel.

Upvotes: 4

toadflakz
toadflakz

Reputation: 7934

Use a ValueConverter to do the translation from ViewModel value of IntFilledPercentage filled to View value of Height for the rectangle.

You would bind the IntFilledPercentage ViewModel property to the Rectangle's Height property and then do the conversion from percentage to actual visual units in the Converter class.

<Rectangle x:Name="oilLevel" Fill="Green" Height="{Binding IntFilledPercentage, Mode=OneWay, Converter={StaticResource PercentageToHeightConverter}" Stroke="Black" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" DockPanel.Dock="Bottom"/>

Converters implement the IValueConverter interface. In this case you only need to implement Convert().

Upvotes: 1

Related Questions