4

Got a tough one. Consider a ViewModel that is comprised of a list of objects, where each object defines an int value, and some of those objects also define a Presets dictionary of ints keyed on a 'friendly' string representing that value in the UI.

Here's an example...

List<SomeItem> AllItems;

public class SomeItem : INotifyPropertyChanged
{
    public SomeItem(int initialValue, Dictionary<int,string> presets)
    {
        this.CurrentValue = initialValue;
        this.Presets = presets;
    }
    public int CurrentValue{ get; set; } // Shortened for readability. Assume this property supports INPC
    public Dictionary<int,string> Presets{ get; private set; }
}

The goal for the UI is if the item has no presets, the user can enter any int value they want. However, if there are presets, we want to limit them to those values and also display them in the UI as the Friendly names from the dictionary.

Our first attempt was to use a TextBox and a ComboBox, modifying their visibilities depending on if there were presets or not, like this...

<ComboBox ItemsSource="{Binding Presets}"
    DisplayMemberPath="Key"
    SelectedValuePath="Value"
    SelectedValue="{Binding CurrentValue, Mode=TwoWay}"
    Visibility={Binding HasPresets, Converter=...}">

<TextBox Text="{Binding CurrentValue}"
    Visibility={Binding HasPresets, Converter...}" /> // Assume the inverse of the combo

...but when we're using this in a DataTemplate of a list that supports virtualization, the combo occasionally displays blanks. I believe is because when the item is reused and the DataContext changes, SelectedValue updates before ItemsSource meaning there's potentially no preset value to match on, thus the proposed SelectedValue value gets tossed by the control, then ItemsSource updates, but there's no selected value so it shows a blank.

My next thought (and what we preferred anyway) was to use only a TextBox that displayed the preset name but was actually bound to Value, then use a converter to work its magic, and let the user type either the friendly name or the actual value. If the user typed something that wasn't a valid value or a preset, we'd just throw an error. If there were no presets, it would simply act as a pass-through of the value.

However, there I'm not sure how to pass in the presets to the converter. You can't set a binding on a ConverterParameter to pass them in that way, and if you use a multi-binding, then I'm not sure how to structure the 'ConvertBack' call since there too I need them passed in, not sent back out.

I'm thinking the proper way is to implement UiValue in the ViewModel which we'd simply bind to like this...

<TextBox Text="{Binding UiValue}" />

...then move the code that would've been in the converter to that property's getter/setter implementation, or simply be a pass-through to Value if there are no presets. However, this seems like too much logic is going in the ViewModel where it should be in the View (ala a converter or similar.) Then again, maybe that's exactly the point of the ViewModel. I dunno. Thoughts welcome.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286

2 Answers2

2

Personally, I would go for putting the 'converter code' into the property as you suggested... I don't see any problem with having the code in there. In fact, it's probably better than having it in a Converter because then you can easily test it too.

Sorry, this isn't much of an answer, but I felt that your question deserved at least one.

Sheridan
  • 68,826
  • 24
  • 143
  • 183
1

I like your question, because it illustrates the way of thinking that stands behind the existence of a ViewModel in WPF. Sometimes they seem inevitable.

Converters are designed to be stateless, which is why it's difficult to pass context variables like presets. ViewModel is a layer, of which responsibility is to prepare Model for binding purposes. The role of a "model" is to handle logic. Thus, a ViewModel may handle in detail the behaviour (logic) of a View. This is precisely what you want. Most of the time I find myself not needing Converters at all.

Sometimes it feels more natural that the view logic should be in the View, but then ViewModel seems superfluous. However, when that logic is located in the ViewModel it's usually easier to auto-test. I wouldn't be afraid of putting stuff like this in ViewModel at all. Often this is the easiest (and correct) way.

Have UiValue property in ViewModel and handle conversion there:

public string UiValue{ get{/*...*/} set{/*...*/} }

To rephrase, in WPF there is no clean way to replace the property you bind to. E.g. if you wanted to have

<TextBox Text="{Binding IntValue}" />

change at some point to:

<TextBox Text="{Binding PresetValue}" />

you're trapped. This is not how things are done. Better have a constant binding like

<TextBox Text="{Binding UiValue}" />

and deal with the logic behind the UiValue property.

Another possible approach (instead of playing with visibility of ComboBox and TextBox) is to have a DataTemplateSelector, which would decide whether a ComboBox or TextBox should be created for SomeItem. If presets are null or empty select TextBox-based DataTemplate, otherwise take ComboBox. If I'm not mistaken you'd have to investigate FrameworkElement.DataContext property from within the selector to find the context (presets).

Considering your doubt about ConvertBack method, most commonly value or Binding.DoNothing is returned in case you don't need conversion in any of the directions.

pbalaga
  • 2,018
  • 1
  • 21
  • 33
  • The DataTemplateSelector code would have the same issue I'm facing now... if the value is set before the ItemsSource, the value gets tossed out. (I confirmed this in a 'Playground' app I use for such tests.) I think the trick is to bind to a property defined on the View, then in the code-behind I set the actual bindings on the ComboBox, etc. so I can ensure they are set the correct way. Actually, I may forego binding directly to the combo box and do everything in the View code-behind setting that view-defined property, which *would* be bound to the ViewModel. Still, it's a fun exercise. – Mark A. Donohoe Sep 11 '13 at 20:00