다음을 통해 공유


Reusing ViewModels in a Universal App – Part 4

To restate where part 3 ended – We have migrated all the buttons and TextBox to use the ViewModel.  What is left is the display of the items on the stack and the mode radio buttons.

Handling Radio Buttons

There are a few different ViewModel patterns which work with RadioButtons.

Option #1 – Since RadioButton supports Command we could make a ChangeModeCommand which takes a parameter of the new mode. 

The pros of this solution are:

· It works well if other supported views uses controls which support commands for changing modes (i.e. some kind of button style)

The cons are:

· Can’t set initial button state

· Doesn’t update state of other buttons 

· CanExecute should always return true, otherwise radio buttons will get disabled

Those issues can be solved, but will require more work.  Which implies we should only go this route if we need commands for the other view implementations.  At this point we haven’t designed the phone UI, so going with a low cost solution is probably best.  Plus I’d like to demonstrate some other techniques.

The next 2 options are variations on 2 way bindings.

Options #2 – Add an IsHexMode and IsDecimalMode bool properties to the ViewModel and 2 way bind the RadioButton.IsChecked to them. 

The pros of this solution are:

· Very simple and easy to understand

· Allows configuring initial control state

· Updates control state of all RadioButtons

The cons are:

· Only works well for RadioButton UI

· Doesn’t scale if the number of modes increases

Option #3 – 2 way bind to the Mode property and use a converter to convert back and forth between CalculatorMode and bool.

The pros of this solution are:

· Just 1 property regardless of number of modes

· Allows configuring initial control state

· Updates control state of all RadioButtons

· Different (or none) converters can be used for other UIs (i.e. enums work well with ComboBoxes)

The cons are:

· Have to be careful that we have 1 “winner” when buttons change (i.e. 1 RadioButton should change the mode, the others shouldn’t even though their IsChecked property changes)

 

In this example we are going with Option #3 because not only is it a good choice, but it allows me to demonstrate a new ViewModel technique.  To implement a converter we define a class and have it implement IValueConverter.  Since we will be using it in 2 way bindings we implement both Convert and ConvertBack.

If this was WPF we’d be able to pass the enum value in as the ConverterParameter, but Windows store apps don’t support x:Static like WPF does, so we need to pass the value in as a string.  Then inside the converter parse the string into the right mode enum value.

Here is the implementation:

    publicclassBoolToCalculatorModeConverter : IValueConverter

    {

        public BoolToCalculatorModeConverter()

    {

            // nothing

        }

 

        publicobject Convert(object value, Type targetType, object parameter, string language)

        {

            var desiredMode = StringToMode((string)parameter);

 

            var acutalMode = (CalculatorViewMode)value;

 

            return (desiredMode == acutalMode);

        }

 

        publicobject ConvertBack(object value, Type targetType, object parameter, string language)

        {

            var state = (bool)value;

            var desiredMode = StringToMode((string)parameter);

 

            if(state)

            {

                return desiredMode;

            }

            else

            {

                // since we don't know the right value, just return null

                returnnull;

            }

        }

 

        CalculatorViewMode StringToMode(string value)

        {

            switch(value)

            {

                case"Decimal":

                    returnCalculatorViewMode.Decimal;

                case"Hex":

  returnCalculatorViewMode.Hex;

                default:

                    thrownewNotSupportedException();

            }

        }

    }

 

You’ll notice in ConvertBack we solve the issue of the unchecked RadioButtons from changing the mode by returning null, which isn’t a valid enum value.  That causes the binding to fail and not update the property.

Then in the XAML we first declare an instance of the converter in the Resources section, and then update the RadioButton to bind to the value using the Converter.  Here is the code for one of the RadioButtons:

            <RadioButton

                x:Name="_HexSelection"

                Grid.Column="0"

                Grid.Row="1"

                Style="{StaticResource RadioButtonStyle}"

                Content="Hex"

                GroupName="NumbericType"

                IsChecked="{Binding Mode, Converter={StaticResource BoolToCalculatorModeConverter}, ConverterParameter=Hex, Mode=TwoWay}"

                />

 

And of course we need to go into our View and rip out all the code which interacted with the RadioButtons.

Picking a Solution for Stack Content Display

Now we are down to the last bit of View code – that dealing with populating the ListBox with the stack contents, formatting appropriately for the mode.

If this was WPF we could use a MultiBinding and IMultiValueConverter.  That would allow us to bind to the number and mode, and in the converter format the value appropriately and return it.  It would be re-evaluated when the values of the bindings change.  However this isn’t available for Windows Store apps currently.

We could also solve this with a custom control for the content for the ListBoxItems.  But that seems like a heavy handed solution.  It also limits reusability as other Views may not be able to re-use that control for whatever reason.

Another way is to expose a collection of strings from the ViewModel and then update this collection when the mode changes and when the stack changes.  This can be a reasonable solution and is pretty straightforward to implement.  Something like a ConvertingReadOnlyObservableCollection can help make that happen.  When the mode changes you could either re-build the shadow collection with newly formatted strings.  Or the collection could be a ViewModel class with a string property for the formatted string.  Then when the mode changes one can enumerate the collection and tell each object to re-format, which as it does will raise a PropertyChanged event to cause the bindings to update.

I’m going to go with a different solution, which uses an Attached Service (some people call it an Attached Behavior).  If you haven’t had experiences with these then it may feel weird and require some getting used to.  It will allow the ItemsControl and bindings do the work, rather than us having to explicitly code it.

Attached Services

You may be aware that XAML controls use a thing called DependencyProperty.  This allows controls to do stuff like inherit property values from their parent controls in a seamless way for us using the controls.  A closely related thing is an AttachedDependencyProperty.  This is a property defined on one class, but you actually set/retrieve the values on instances of other classes – weird, right?

I’ll not go into how this is implemented internally, just believe that it does work and allows us to do some things which are usually unconventional.

I bet you’ve used these before and never realized you were using attached properties.  For example if you add Grid.Column=”3” onto a TextBox then you are setting the value for an attached property.  The Grid implementation then reads these values when performing layout.

One of the tricky things you can do with an attached property is register a callback for when the property is changed.  This callback gets as the sender the object that the property has been applied to.  At that point you can interact with that object – get/set properties, call methods, etc...  It is typically the implementation of the changed callback that turns this into a “service” instead of just a stored value.

What is great about attached services is that if written right they can apply to just about any control, even custom ones.  And you can apply different attached services to the same control instance, allowing you to combine behaviors.  This is very superior to traditional methods like sub-classing or custom controls.

CalculatorNumberFormatter

The attached service we are going to implement is CalculatorNumberFormatter.  It will define 3 attached properties.  2 are input properties – Number and Mode.  Number is designed to be bound an integer value.  Mode is designed to be bound to the current Mode.  When either of those properties change it will run code which will correctly format the integer into a string based on the mode.  The resulting value will be placed in an output parameter called Text.

Then the control that these properties are attached to can bind it’s Content/Text property to CalculatorNumberFormatter.Text. 

Not only will this correctly format the number correctly initially, but it will respond if the mode changes.  And this will happen in a better way than the initial View oriented implementation we had, because it will not clear and rebuild the listbox.  It also works if the ListBox (or any ItemsControl) is virtualizing (but we will not dive into that in this series).

Before we look at the code I’d like to point out that we could have avoided the Text property and instead casted the DependencyObject we are attached to into what control it actually is, like TextBlock.  However this limits reuse as it wouldn’t work with other controls.

Here is the code for one of the attached properties.  You’ll notice we both define the static property, and add Get/Set methods.  Those methods are helpful, but also required for the XAML to compile.

        publicstaticreadonlyDependencyProperty ModeProperty = DependencyProperty.RegisterAttached(

                                                                        "Mode",

                                                                        typeof(CalculatorViewMode),

                                                                        typeof(CalculatorNumberFormatter),

                                                                        newPropertyMetadata(null, ModeProperty_PropertyChanged));

 

 

        publicstaticCalculatorViewMode GetMode(DependencyObject obj) { return (CalculatorViewMode)obj.GetValue(ModeProperty); }

        publicstaticvoid SetMode(DependencyObject obj, CalculatorViewMode value) { obj.SetValue(ModeProperty, value); }

 

        staticprivatevoid ModeProperty_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)

        {

            FormatNumber(o);

        }

 

The Number and Text properties are similar – just different names and types.

FormatNumber gets the Number and Mode values (handling if they aren’t set) and sets the Text property.

        staticvoid FormatNumber(DependencyObject o)

        {

            var mode = (CalculatorViewMode?) o.GetValue(ModeProperty);

            var value = (Int64?)o.GetValue(NumberProperty);

 

            if (mode.HasValue && value.HasValue)

            {

                string text;

 

                if (mode == CalculatorViewMode.Hex)

                {

                    text = String.Format(CultureInfo.CurrentCulture, "0x{0:X}", value.Value);

                }

                else

                {

                    text = String.Format(CultureInfo.CurrentCulture, "{0:G}", value.Value);

                }

 

                SetText(o, text);

            }

        }

 

Applying CalculatorNumberFormatter to the ListBox

In order to apply this attached service to the ListBox we start off by binding the ItemsSource property to the Stack collection.  Logic inside the control will create a child control for each item.  We also set the ItemTemplate property so we can control the details of what those child controls expose.

One thing to be aware of is that the DataContext for the ListBoxItem children is not the ViewModel, but a specific item from the ItemsSource collection – in this case an int from our Stack collection.  So binding without a Path value will give use that object.  To get a reference to our view model, which has the Mode property we need, we need to use some additional features of binding.  Our choices in a Windows Store app are more limited than in WPF, but we only need 1 way that works.  In this case using ElementName and specifying an element outside the ListBox will work nicely.

That handles binding the Number and Mode properties.  All that is left is to connect TextBlock.Text to CalculatorNumberFormatter.Text.  I tried doing this the typical way and was having issues getting it to work.  So I wound up reversing it and making the binding 2 way to feed the value to the right place.

The result is this:

            <ListBox

       x:Name="_StackBox"

                Grid.Column="0"

                Grid.ColumnSpan="2"

                Grid.Row="0"

                Margin="4"

                FontSize="32"

                ItemContainerStyle="{StaticResource ListBoxItemStyle}"

                ItemsSource="{Binding Calculator.Stack}"

                >

                <ListBox.ItemTemplate>

                    <DataTemplate>

                        <TextBlock

                            local:CalculatorNumberFormatter.Number="{Binding}"

                            local:CalculatorNumberFormatter.Mode="{Binding DataContext.Mode, ElementName=StackGrid}"

                            local:CalculatorNumberFormatter.Text="{Binding Text, RelativeSource={RelativeSource Mode=Self}, Mode=TwoWay}"

                            />

                    </DataTemplate>

                </ListBox.ItemTemplate>

            </ListBox>

 

We can then clean out the ListBox related code out of the view.

Wrapping it Up

We are now done with the first phase.  Our MainPage class no longer has any logic in it.  The only thing we have in there that isn’t part of the VS broiler plate is the code to create the ViewModel and assign the DataContext.

We learned a few new techniques to decoupling logic so we can have a View independent ViewModel and maximize code reusability.

· ValueConverters can be used to bridge the gap between how our ViewModel works and the needs of a specific control

· Attached services are a powerful technique to connect custom logic to any control, helping us customize behaviors and overcome system limitations

In the next edition we’ll take this completed ViewModel and build a phone view on top of it – proving that we can re-use it for different Views.

 

MyCalc - part 3.zip