views:

479

answers:

5

I have a WPF window with a textbox, using standard WPF Databinding to an object. This all works fine, the issue is this:

The user is entering a time estimate, I would like to give the user the option to enter the data in hours or minutes (through a drop down list). I'd like to keep the data in minutes, and if the user selects hours, multiply their input by 60 and store the data.

How can I achieve that with WPF databinding? Is this even possible?

edit

For example, 90 minutes would show up as 1.5 if the dropdown list is set to hours, but 90 if miutes is selected.

+3  A: 

You could use a special property on your window:

<ComboBox x:Name="Units">
    <sys:String>Hours</sys:String>
    <sys:String>Minutes</sys:String>
</ComboBox>
<TextBox x:Name="Value" Text="{Binding Path=Estimate, ElementName=ThisWindow}" />

And then implement the special property:

public double Estimate
{
    get
    {
        switch(this.Units.SelectedItem as String)
        {
        case "Hours":
            return this.estimate / 60.0;
        default:
            return this.estimate;
        }
    }

    set
    {
        switch(this.Units.SelectedItem as String)
        {
        case "Hours":
            this.estimate = value * 60.0;
            break;
        default:
            this.estimate = value;
            break;
        }

        this.OnPropertyChanged("Estimate");
    }
}
sixlettervariables
Meta-comment: in our WPF applications we implement units selection as a part of the ViewModel which handles the conversion to and from the selected units for all members of the ViewModel (granted this is to handle Metric v. Imperial). This allows us to have each ViewModel use its own units. When we want multiple ViewModel's to share the same units, we just bind them to the same ComboBox.
sixlettervariables
+1  A: 

You can use a MultiValueConverter :

XAML

        <ComboBox ItemsSource="{StaticResource values}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock>
                      <TextBlock.Text>
                        <MultiBinding Converter="{StaticResource minutToHours}">
                          <Binding/>
                          <Binding Path="IsChecked" ElementName="chkHours"/>
                        </MultiBinding>
                      </TextBlock.Text>
                    </TextBlock>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <CheckBox Name="chkHours" Content="Show hours"/>

C# code for Converter

public class MinuteToHoursConverter : IMultiValueConverter
{
    #region IMultiValueConverter Members

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        int minutes = 0;
        if (values[0] is TimeSpan)
        {
            minutes = (int)((TimeSpan)values[0]).TotalMinutes;
        }
        else
        {
            minutes = System.Convert.ToInt32(values[0]);
        }
        bool showHours = System.Convert.ToBoolean(values[1]);
        if (showHours)
            return (minutes / 60.0).ToString();
        else
            return minutes.ToString();
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotSupportedException();
    }

    #endregion
}

But this solution is probably not optimal... IMHO the best way if you're using the MVVM pattern is to create an extra property in the ViewModel to compute the correct value

Thomas Levesque
The problem is the ConvertBack to take the time somebody enters in as either Hours or Minutes and convert it to Minutes.
sixlettervariables
+3  A: 

After first completely misunderstanding the question, here's my effort at getting my first bounty ;)

<Window.Resources>        
    <local:DivisionConverter x:Key="HoursConverter" Divisor="60"/>

    <DataTemplate DataType="{x:Type local:MyObject}">
        <StackPanel Orientation="Horizontal">
            <TextBox x:Name="InputBox" Text="{Binding Path=Estimate}" Width="80"/>
            <ComboBox x:Name="InputUnit" SelectedItem="Minutes">
                <System:String>Minutes</System:String>
                <System:String>Hours</System:String>
            </ComboBox>
        </StackPanel>            
        <DataTemplate.Triggers>
            <DataTrigger Binding="{Binding ElementName=InputUnit, Path=SelectedItem}" Value="Hours">
                <Setter TargetName="InputBox" Property="Text" Value="{Binding Path=Estimate, Converter={StaticResource HoursConverter}}"/>
            </DataTrigger>
        </DataTemplate.Triggers>
    </DataTemplate>        
</Window.Resources>
<Grid>
    <ContentControl VerticalAlignment="Top">
        <local:MyObject Estimate="120"/>
    </ContentControl>
</Grid>

In general i dislike specialized converters: after a while you lose track of which converter does what exactly, and you end up going through converter code everytime you think you need one, or worse you build a second one which does exactly the same. So now i'm only using them when all else fails.

Instead i defined a general purpose DivisionConverter, which divides the value in the Convert method, and multiplies the value in the ConvertBack method, exactly as you'd expect, no need to look at the code here ;)

Additionaly i use a DataTemplate and Triggers where others used a MultiBinding or (imo worse) added this UI logic in the getter and setter. This keeps all this relatively simple UI logic in one overseeable place.

Now just to be complete this is the code-behind:

public class MyObject
{
    public double Estimate { get; set; }
}


public class DivisionConverter : IValueConverter
{
    public double Divisor { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        double doubleValue = (double)value;
        return doubleValue / Divisor;            
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {           
        double inputValue;
        if (!double.TryParse((string)value, NumberStyles.Any, culture, out inputValue)) 
            return 0;

        return inputValue * Divisor;
    }
}
Bubblewrap
This looks interesting... With your method, you specify a proprty in XAML for how you want to use the specific instance of the Divisor... I like minimizing the posibility of reinventing existing code.
Nate Bross
A: 

Maybe I'm overthinking this, but since a combobox has both a Display Value, and an Actual value (behind-the-scenes from the user), why not have your estimate list have two columns... both the hours and minutes respectively. Then, based on whichever mode is checked, just change the "Display" binding to the respective one. This way, it will always store the behind-the-scenes minutes number you wanted. No other conversion to/from.

DRapp