none
Get at Binding from inside IValueConverter?

    Question

  • I am writing an IValueConverter that needs access to more than just the value to convert. Specifically, I need access to the DataContext (source object) and Path (property name) so I can check to see whether the user is authorized to view the data. If they aren't, I want the converter to return "n/a" or some similar neutral value.

    Obviously this sort of thing is somewhat possible, because the ExceptionValidationRule is aware of the data flow and whether an exception occurred.

    But it isn't obvious to me, or google, how you might get at these details from inside the Convert() or ConvertBack() methods.

    Anyone have any ideas?

    Thursday, January 18, 2007 10:55 PM

Answers

  • Use an IMultiValueConverter whose inputs are (a) the real data, (b) the target element, and (c) the target property.  The last input is constant, so it can be transmitted as a converter parameter.  Use the target element to get the DataContext, and use the target property to look up the binding path.  Here's a sample that you should be able to adapt to your scenario:

    XAML:
    <TextBlock DataContext="{Binding ElementName=tb, Path=Text}">
      <
    TextBlock.Text
    >
        <
    MultiBinding Converter="{StaticResource MyConverter}" ConverterParameter="{x:Static TextBlock.TextProperty}"
    >
          <
    Binding
    />          <!-- this is the binding to the "real" data - in your scenario it may have its own path, converter, etc. -->
          <
    Binding RelativeSource="{x:Static RelativeSource.Self}"
    />
        </
    MultiBinding
    >
      </
    TextBlock.Text
    >
    </TextBlock>

    Code:
    public class MyConverter : IMultiValueConverter
    {
     
    #region IMultiValueConverter Members
     
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
       
    string rawValue = (string)values[0];
       
    DependencyObject targetElement = (DependencyObject)values[1];
       
    DependencyProperty dp = (DependencyProperty)parameter;

        object dataContext = targetElement.GetValue(FrameworkElement.DataContextProperty);

        MultiBindingExpression mbe = targetElement.ReadLocalValue(dp) as MultiBindingExpression;
       
    MultiBinding mb = mbe.ParentMultiBinding;
       
    Binding binding = mb.Bindings[0] as Binding;

        return (IsAccessAllowed(dataContext, binding.Path.Path)) ? rawValue : "n/a";
      }

      public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
      {
       
    throw new NotSupportedException("The method or operation is not implemented.");
      }

      private bool IsAccessAllowed(object dataContext, string path)
      {
       
    string s = dataContext as String;
       
    return (s != null && s.StartsWith("abc"));    // your logic goes here
      }
     
    #endregion
    }

    The code to get the MultiBinding should really be written as
       MultiBinding mb = BindingOperations.GetMultiBinding(targetElement, dp);
    but that didn't work for some reason (it returned null).  I fear it's a bug, but I can't tell for sure from here (I'm not in the office).  The code in the sample works for local bindings, but not for bindings defined in a style or template.  GetMultiBinding is supposed to work in all cases.

    I'm kind of embarassed that this works, since converters are supposed to be stateless and ignorant of their context, in order to reduce the possiblity of side-effects that make the app difficult to debug and maintain.  Your scenario seems fine, but I could imagine this technique being abused.  Oh well, too late now.

    Friday, January 19, 2007 5:34 AM

All replies

  • Use an IMultiValueConverter whose inputs are (a) the real data, (b) the target element, and (c) the target property.  The last input is constant, so it can be transmitted as a converter parameter.  Use the target element to get the DataContext, and use the target property to look up the binding path.  Here's a sample that you should be able to adapt to your scenario:

    XAML:
    <TextBlock DataContext="{Binding ElementName=tb, Path=Text}">
      <
    TextBlock.Text
    >
        <
    MultiBinding Converter="{StaticResource MyConverter}" ConverterParameter="{x:Static TextBlock.TextProperty}"
    >
          <
    Binding
    />          <!-- this is the binding to the "real" data - in your scenario it may have its own path, converter, etc. -->
          <
    Binding RelativeSource="{x:Static RelativeSource.Self}"
    />
        </
    MultiBinding
    >
      </
    TextBlock.Text
    >
    </TextBlock>

    Code:
    public class MyConverter : IMultiValueConverter
    {
     
    #region IMultiValueConverter Members
     
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
      {
       
    string rawValue = (string)values[0];
       
    DependencyObject targetElement = (DependencyObject)values[1];
       
    DependencyProperty dp = (DependencyProperty)parameter;

        object dataContext = targetElement.GetValue(FrameworkElement.DataContextProperty);

        MultiBindingExpression mbe = targetElement.ReadLocalValue(dp) as MultiBindingExpression;
       
    MultiBinding mb = mbe.ParentMultiBinding;
       
    Binding binding = mb.Bindings[0] as Binding;

        return (IsAccessAllowed(dataContext, binding.Path.Path)) ? rawValue : "n/a";
      }

      public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
      {
       
    throw new NotSupportedException("The method or operation is not implemented.");
      }

      private bool IsAccessAllowed(object dataContext, string path)
      {
       
    string s = dataContext as String;
       
    return (s != null && s.StartsWith("abc"));    // your logic goes here
      }
     
    #endregion
    }

    The code to get the MultiBinding should really be written as
       MultiBinding mb = BindingOperations.GetMultiBinding(targetElement, dp);
    but that didn't work for some reason (it returned null).  I fear it's a bug, but I can't tell for sure from here (I'm not in the office).  The code in the sample works for local bindings, but not for bindings defined in a style or template.  GetMultiBinding is supposed to work in all cases.

    I'm kind of embarassed that this works, since converters are supposed to be stateless and ignorant of their context, in order to reduce the possiblity of side-effects that make the app difficult to debug and maintain.  Your scenario seems fine, but I could imagine this technique being abused.  Oh well, too late now.

    Friday, January 19, 2007 5:34 AM
  • Thanks Sam!  This is so sweet.
    Friday, January 19, 2007 1:19 PM
  • Thank you Sam, I appreciate the help and this appears to be what I'm after.

    However, I understand your concern about misusing a converter like this.

    Given my requirement, which is to prevent returning data if it is not authorized, do you have a better suggestion for a way to accomplish this goal?

    Perhaps I could do it in the code-behind if there's a way to enable/disable a binding at runtime? In other words, perhaps I could just have code in the page that checks the authorization and if the user isn't authorized it could just temporarily disable the binding to prevent the value from appearing?

    Obviously I'd need to be able to dynamically reenable the binding later, because the user could log in, or switch identities to a more priviledged one that could see the value.

    Friday, January 19, 2007 3:03 PM
  • My initial thought is that you can use the attached DPs to achieve this, imagine that you can name this attached DPs as IsAuthorizedProperty, and place a CLR IsAuthroized property wrapper around it, then in the xaml, you can use the property trigger to enable and disable binding based on the true/false value of this property:

    <Trigger Property="AuthroizationManager.IsAuthorized" Value="True">
      <Setter Property="DataContext" TargetName="fooElement" Value="{Binding Path=dataSource}"/>
    </Trigger>
    <Trigger Property="AuthroizationManager.IsAuthorized" Value="False">
      <Setter Property="DataContext" TargetName="fooElement" Value="{x:Null}"/>
    </Trigger>

    Sheva
    Friday, January 19, 2007 4:27 PM
  • There's no way to disable a binding at runtime.  You can remove it and reinstall it later.  You can tweak its inputs as Sheva suggested.  (Of course if there's an IsAuthorized property available, you can simply include it in the inputs to the MultiValueConverter;  this is more efficient than DataTrigger.)    But you can't leave the binding lurking silently - if it exists, it will contribute to the computation of its target property.

    I gather that your authorization check depends on which source property is being queried, so that a single "IsAuthorized" property isn't granular enough.  In that case, I don't have a better idea.  Your use of my converter technique is perfectly safe.  It's a clean function with no side-effects (i.e. it doesn't alter the tree, change source properties, add/remove bindings, etc.), so go ahead and use it.

    Friday, January 19, 2007 9:32 PM
  • If you want a value converter to "not return anything" you can make it return Binding.DoNothing.  Then the binding system will not transfer the binding source value to the target (this is different from returning null from the Convert method).  Does that help you?
    Friday, January 19, 2007 10:17 PM
  • Another way to do it is using the ConverterParameter property of a binding. Something like:

    Binding b = new Binding();
    b.Converter = new MyConverter();
    b.ConverterParameter = b;
    ...

    You can then use it directly inside the converter:

    public class MyConverter : IValueConverter {
       public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
          Binding b = (Binding)parameter;
          if (b.Path == "abc") {
             // custom logic
          } 
          ...
       }
       ...
    }


     

    P.S.,

    I totally agree that a converter should not depend on state, but this is not related to whether or not parameters are passed to the converter. You can create a converter that depends on state even if it doesn't get any parameters other then the value.

    I don't think that passing parameters to converters is wrong. It can actually help in reducing the number of converters you have to write in many cases.

     

     

    Monday, January 22, 2007 12:30 PM
  • I think, I found the right way:

    My XAML that pass data context(whole row) to the converter and we can find out which column in process by ConverterParameter:

            <sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}"

                   VerticalAlignment="Stretch" HorizontalAlignment="Stretch"

                   Width="Auto" Name="DataGrid1"

                   MinHeight="116" MaxHeight="600" LoadingRow="DataGrid1_LoadingRow">

             

                  <sdk:DataGrid.Columns>

               

                <sdk:DataGridTextColumn Header="Type"

              Binding="{Binding Mode=OneTime, Converter={StaticResource CellValueConverter1},ConverterParameter=objtype}"

              CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" />



                <sdk:DataGridTextColumn Header="Id"

              Binding="{Binding Path=objid}"

              CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" />



                <sdk:DataGridTextColumn Header="Action"

              Binding="{Binding Mode = OneTime,Converter={StaticResource CellValueConverter1},ConverterParameter=action}"

              CanUserReorder="True" CanUserResize="True" CanUserSort="True" Width="Auto" />



              </sdk:DataGrid.Columns>

            </sdk:DataGrid>

     

    Converter itself is standart except one detail,property where I store parent:

    

     

      public class CellValueConverter : IValueConverter

      {

        Events temp = null;

        public Events ParentPage

        {

          get { return temp;}

          set{temp = value;}

        }



        public CellValueConverter()

        {

        }

        public object Convert(object value, Type targetType, object parameter1,

        System.Globalization.CultureInfo culture)

        {

    The most tricky part for me was to find how to pass parent after initialization. It's done this way:

    //constructor:

    public Events()

        {

          InitializeComponent();

          DataGridTextColumn col = (DataGridTextColumn)DataGrid1.Columns[2];

          MyConvertor = (CellValueConverter)

            ((System.Windows.Controls.DataGridBoundColumn)(col)).Binding.Converter;

         

          if (MyConvertor != null)

          {

            MyConvertor.ParentPage = this;

          }

    //Now we can use parent and its members in convertor

        }



    Tuesday, August 09, 2011 12:54 PM