views:

62

answers:

2

I'm new to WPF and MVVM. I'm struggling to determine the best way to change the view of a chart. That is, initially a chart might have the axes: X - ID, Y - Length, and then after the user changes the view (either via lisbox, radiobutton, etc) the chart would display the information: X - Length, Y - ID, and after a third change by the user it might display new content: X - ID, Y - Quality.

My initial thought was that the best way to do this would be to change the bindings themselves. But I don't know how tell a control in XAML to bind using a Binding object in the ViewModel, or whether it's safe to change that binding in runtime?

Then I thought maybe I could just have a generic Model that has members X and Y and populate them as needed in the viewmodel?

My last thought was that I could have 3 different chart controls and just hide and show them as appropriate.

What is the CORRECT/SUGGESTED way to do this in the MVVM pattern? Any code examples would be greatly appreciated.

Thanks

Here's what I have for the bind to bindings method:

XAML:

        <charting:Chart.Series>
            <charting:BubbleSeries Name="bubbleSeries1"
                                   ClipToBounds="False"
                                   model:MakeDependencyProperty.IndependentValueBinding="{Binding AxisChoice.XBinding}"
                                   model:MakeDependencyProperty.DependentValueBinding="{Binding AxisChoice.YBinding}"
                                   model:MakeDependencyProperty.SizeValueBinding="{Binding AxisChoice.SizeBinding}"
                                   IsSelectionEnabled="True" SelectionChanged="bubbleSeries1_SelectionChanged"
                                   ItemsSource="{Binding Data}">
            </charting:BubbleSeries>
        </charting:Chart.Series>

        <ComboBox Height="100" Name="listBox1" Width="120" SelectedItem="{Binding AxisChoice}">
            <model:AxisGroup XBinding="{Binding Performance}" YBinding="{Binding TotalCount}" SizeBinding="{Binding TotalCount}" Selector.IsSelected="True"/>
            <model:AxisGroup XBinding="{Binding ID}" YBinding="{Binding TotalCount}" SizeBinding="{Binding BadPerformance}"/>
            <model:AxisGroup XBinding="{Binding ID}" YBinding="{Binding BadPerformance}" SizeBinding="{Binding TotalCount}"/>
        </ComboBox>

AxisGroup:

public class AxisGroup : DependencyObject// : FrameworkElement
{
    public Binding XBinding { get; set; }
    public Binding YBinding { get; set; }
    public Binding SizeBinding { get; set; }
}

DP:

public class MakeDependencyProperty : DependencyObject
{
    public static Binding GetIndependentValueBinding(DependencyObject obj) { return (Binding)obj.GetValue(IndependentValueBindingProperty); }
    public static void SetIndependentValueBinding(DependencyObject obj, Binding value) { obj.SetValue(IndependentValueBindingProperty, value); }
    public static readonly DependencyProperty IndependentValueBindingProperty =
        DependencyProperty.RegisterAttached("IndependentValueBinding", typeof(Binding), typeof(MakeDependencyProperty), new PropertyMetadata { PropertyChangedCallback = (obj, e) => { ((BubbleSeries)obj).IndependentValueBinding = (Binding)e.NewValue;}});


    public static Binding GetDependentValueBinding(DependencyObject obj) { return (Binding)obj.GetValue(DependentValueBindingProperty); }
    public static void SetDependentValueBinding(DependencyObject obj, Binding value) { obj.SetValue(DependentValueBindingProperty, value); }
    public static readonly DependencyProperty DependentValueBindingProperty =
        DependencyProperty.RegisterAttached("DependentValueBinding", typeof(Binding), typeof(MakeDependencyProperty), new PropertyMetadata { PropertyChangedCallback = (obj, e) => { ((BubbleSeries)obj).DependentValueBinding = (Binding)e.NewValue; } });

    public static Binding GetSizeValueBinding(DependencyObject obj) { return (Binding)obj.GetValue(SizeValueBindingProperty); }
    public static void SetSizeValueBinding(DependencyObject obj, Binding value) { obj.SetValue(SizeValueBindingProperty, value); }
    public static readonly DependencyProperty SizeValueBindingProperty =
        DependencyProperty.RegisterAttached("SizeValueBinding", typeof(Binding), typeof(MakeDependencyProperty), new PropertyMetadata { PropertyChangedCallback = (obj, e) => { ((BubbleSeries)obj).SizeValueBinding = (Binding)e.NewValue; } }); 
}

ViewModel:

public class BubbleViewModel : BindableObject
{
    private IEnumerable<SessionPerformanceInfo> data;
    public IEnumerable<SessionPerformanceInfo> Data { ... }

    public AxisGroup AxisChoice;
}

This generates the following exception: + $exception {"Value cannot be null.\r\nParameter name: binding"} System.Exception {System.ArgumentNullException}

Has something to do with the 4 bindings in teh bubbleSeries. I'm more than likely doing something wrong with binding paths but as I said I'm new to binding and wpf, so any tips would be greatly appreciated.

+2  A: 

Your initial thought was correct: You can bind to bindings, for example if you want to change both axes together you might have a ComboBox like this:

<ComboBox SelectedItem="{Binding AxisChoice}">
  <my:AxisChoice XBinding="{Binding ID}" YBinding="{Binding Length}" />
  <my:AxisChoice XBinding="{Binding Length}" YBinding="{Binding ID}" />
  <my:AxisChoice XBinding="{Binding ID}" YBinding="{Binding Quality}" />
</ComboBox>

To make this work you need to declare XBinding and YBinding as CLR properties of type "Binding":

public class AxisChoice
{
  public Binding XBinding { get; set; }
  public Binding YBinding { get; set; }
}

Ideally you could then simply bind the DependentValueBinding or IndependentValueBinding of your chart:

<Chart ...>
  <LineSeries
    DependentValueBinding="{Binding AxisChoice.XBinding}"
    IndependentValueBinding="{Binding AxisChoice.YBinding}" />
</Chart>

Unfortunately this does not work because DependentValueBinding and IndependentValueBinding aren't DependencyProperties.

The workaround is to create an attached DependencyProperty to mirror each property that is not a DependencyProperty, for example:

public class MakeDP : DependencyObject
{
  public static Binding GetIndependentValueBinding(DependencyObject obj) { return (Binding)obj.GetValue(IndependentValueBindingProperty); }
  public static void SetIndependentValueBinding(DependencyObject obj, Binding value) { obj.SetValue(IndependentValueBindingProperty, value); }
  public static readonly DependencyProperty IndependentValueBindingProperty = DependencyProperty.RegisterAttached("IndependentValueBinding", typeof(Binding), typeof(MakeDP), new PropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
    {
      ((DataPointSeries)obj).IndependentValueBinding = (Binding)e.NewValue;
    }
  });

  public static Binding GetDependentValueBinding(DependencyObject obj) { return (Binding)obj.GetValue(DependentValueBindingProperty); }
  public static void SetDependentValueBinding(DependencyObject obj, Binding value) { obj.SetValue(DependentValueBindingProperty, value); }
  public static readonly DependencyProperty DependentValueBindingProperty = DependencyProperty.RegisterAttached("DependentValueBinding", typeof(Binding), typeof(MakeDP), new PropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
      {
        ((DataPointSeries)obj).DependentValueBinding = (Binding)e.NewValue;
      }
  });

}

So your XAML becomes:

<Chart ...>
  <LineSeries
    my:MakeDP.DependentValueBinding="{Binding AxisChoice.XBinding}"
    my:MakeDP.IndependentValueBinding="{Binding AxisChoice,YBinding}" />
</Chart>

If instead you want to change axes separately (two separate ComboBoxes or ListBoxes), you don't need AxisChoice: Simply make the Items or ItemsSource of each ComboBox consist of bindings, and put the a "XBinding" and "YBinding" properties directly in your view model.

Note that if your control exposes a regular property instead of a property of type Binding you can still use this method, but in this case you will use BindingOperations.SetBinding instead of just storing the binding value.

For example, if you want to change the binding of the text in a TextBlock from:

<TextBlock Text="{Binding FirstName}" />

to

<TextBlock Text="{Binding LastName}" />

based on a ComboBox or ListBox selection, you can use an attached property as follows:

<TextBlock my:BindingHelper.TextBinding="{Binding XBinding}" />

The attached property implementation is trivial:

public class BindingHelper : DependencyObject
{
  public static BindingBase GetTextBinding(DependencyObject obj) { return (BindingBase)obj.GetValue(TextBindingProperty); }
  public static void SetTextBinding(DependencyObject obj, BindingBase value) { obj.SetValue(TextBindingProperty, value); }
  public static readonly DependencyProperty TextBindingProperty = DependencyProperty.RegisterAttached("TextBinding", typeof(BindingBase), typeof(BindingHelper), new PropertyMetadata
  {
    PropertyChangedCallback = (obj, e) =>
      BindingOperations.SetBinding(obj, TextBlock.TextProperty, (BindingBase)e.NewValue)
  });
}
Ray Burns
I am indeed working with the WPFToolkit, and so have used that portion of your advice. Unfortunately I'm having binding issues with the axischoice elements.> System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=TotalCount; DataItem=null; target element is 'AxisGroup' (HashCode=66152109); target property is 'SizeBinding' (type 'Binding')I also had to make the AxisChoice object inherit from dependencyobject to get the GetValue and SetValue functions.
c0uchm0nster
My original answer didn't work because Binding.ProvideValue ignores the type of any DependencyProperty. I have updated my answer to fix this.
Ray Burns
A: 

Hi,

I'm trying to simplify things so I made the ItemsSource of a ComboBox (Y1-Axis) consist of an observable collection of bindings, and I put the "YBinding" property directly in the ViewModel and set the public binding property as the combobox SelectedItem.

The the dependentvaluebinding is crashing the app though when using the public Binding SelectedY1:

<ComboBox Height="22" Name="comboBox1" 
            DisplayMemberPath="Source.MetricVarName"                           
            ItemsSource="{Binding AllY1Choices}"   
            SelectedIndex="0" 
            SelectedItem="{Binding SelectedY1, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
</ComboBox>

<chartingToolkit:LineSeries
        ItemsSource="{Binding AllY1Axis}"
        IndependentValueBinding="{Binding AccumDate}"                            
    my:MakeDP.DependentValueBinding="{Binding SelectedY1}">

In the VM:

private Binding _Y1axisChoice = new Binding();
private ObservableCollection<Binding> _allY1Choices = new ObservableCollection<Binding>();
public ObservableCollection<Binding> AllY1Choices
{
    get { return _allY1Choices; }
    set
    {
    _allY1Choices = value;
    OnPropertyChanged("AllY1Choices");
    }
}

private Binding _selectedY1 = new Binding();
public Binding SelectedY1
{
    get { return _selectedY1; }
    set
    {
        if (_selectedY1 != value)
        {
            _selectedY1 = value;
            OnPropertyChanged("SelectedY1");                    
        }
    }
}

In the VM contstructor:

_Y1axisChoice = new Binding("MetricVarID");
_Y1axisChoice.Source = AllY1MetricVars[0];
_selectedY1 = _Y1axisChoice; // set default for combobox

_allY1Choices.Add(_Y1axisChoice);
_Y1axisChoice = new Binding("MetricVarID");
_Y1axisChoice.Source = AllY1MetricVars[1];

_allY1Choices.Add(_Y1axisChoice);

Any thoughts on this? The Binding object "SelectedY1" has Source.MetricID="OldA", and that's a valid value for the dependent value binding.

The error: An exception of type 'System.InvalidOperationException' occurred in System.Windows.Controls.DataVisualization.Toolkit.dll but was not handled in user code

Additional information: Assigned dependent axis cannot be used. This may be due to an unset Orientation property for the axis or a type mismatch between the values being plotted and those supported by the axis.

Thanks

Brian Wells