4

I am trying to set an entire set of controls within a panel to read-only (e.g. if a user has no permission to edit) through data-binding and an attached property. (I am aware of the fact that setting a panel to disabled also disables its children, but this is too much, since it would also disable hyperlinks, lists, etc.)

Basically, the property changed event handler iterates the visual tree and finds all TextBox children and then sets their IsReadOnly property to either true or false. This works, but does not cover the case where the TextBox already has a IsReadOnly setting - either const or binding. For example if a TextBox should always be read-only, then the attached property should not change it to true. Also if the TextBox has a binding that restricts the TextBox to read-only in some cases, the attached property should not blindly set true or false, but rather combine the settings, i.e. if attached property AND textbox binding indicate no read-only, then it is editible, otherwise it is readonly.

How can this be done? This would require to somehow get the current IsReadOnly setting (binding, markup, constant value, ...) and replace it with a wrapper which does the AND-combination. How do I get the current setting/value source for a dependency property? I looked at the following, but don't see how it would address my problem:

        TextBox1.GetValue(TextBoxBase.IsReadOnlyProperty);
        DependencyPropertyHelper.GetValueSource(TextBox1, TextBoxBase.IsReadOnlyProperty);
        TextBox1.GetBindingExpression(TextBoxBase.IsReadOnlyProperty);

Any help would be appreciated.

J.-

EDIT: I am looking for something like

(pseudo-code) 
TextBox1.IsReadOnly := OR(TextBox1.IsReadOnly, GlobalIsReadOnly) 

which now sets the TextBox1.IsReadOnly to true if the GlobalIsReadOnly flag is set or if the TextBox1.IsReadOnly value indicates read-only, be it a binding, markup or const.

user1211286
  • 681
  • 5
  • 17

3 Answers3

3

You could use a DependencyPropertyDescriptor on to hook your IsReadonly property changed handler (for all objects).

(beware: a handler added to DependencyPropertyDescriptor is a gcroot... keep that in mind to avoid memory leaks)

This hook would try to get your custom attached property, and if it's found and is set to 'readonly forced', re-set your IsReadOnly property to false if it's value is changed (but store a flag, maybe on another attached property, to know if it must be restored to read-only later).

However, your logic would override any binding on IsReadonly. But the same logic could be applied with binding expressions (and not only values of the property) using GetBindingExpression and storing/restoring binding expressions set on IsReadonly property.

pros: no further code required once this is implemented.

cons: DependencyPropertyDescriptor.AddValueChanged "hides" logic... since there will be no clue that this IsReadonly property will be bound to something in further xaml you will write.

* EDIT : other solution *

Using multibinding, this should work (not tested). However, this has some requirements:

  • Bindings/values must1 no be modified
  • Bindings must be intialized before executing this

        var readonlyGlobalBinding = new Binding
            {
                Source = myRoot, // to fill
                Path = new PropertyPath(IsGlobalReadOnlyProperty)
            };
        var be = box.GetBindingExpression(TextBoxBase.IsReadOnlyProperty);
        if (be != null)
        {
            var mb = new MultiBinding();
            mb.Bindings.Add(be.ParentBinding);
            mb.Bindings.Add(readonlyGlobalBinding);
            mb.Converter = new OrConverter();
            box.SetBinding(TextBoxBase.IsReadOnlyProperty, mb);
        }else if(!box.IsReadOnly)
            box.SetBinding(TextBoxBase.IsReadOnlyProperty, readonlyGlobalBinding);
    

using class

        class OrConverter : IMultiValueConverter
    {
        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return values.OfType<bool>().Aggregate(false, (a, b) => a || b);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            throw new InvalidOperationException();
        }
    }
Community
  • 1
  • 1
Olivier
  • 5,578
  • 2
  • 31
  • 46
  • I am not worried about getting notified on property changes. What I am looking for is a way to replace an elements property with something that adds logic to the binding (logical AND) and still uses the original value, so it considered what the property was originally set to (binding, markup, ...). Something like TextBox1.IsReadOnly := AND(TextBox1.IsReadOnly, GlobalIsReadOnly) – user1211286 Nov 18 '13 at 14:15
  • That's what I understood. And I answered that you could fake this behavior via hooking property changed :) Or using multibinding. Let me edit my answer – Olivier Nov 18 '13 at 14:43
  • The multibinding with OrConverter looks like a good solution. I guess I could also write my own OrBinding class that wraps this together. Will give it a try. Thanks! – user1211286 Nov 21 '13 at 16:15
2

It's not exactly what you're after, but I'd approach this problem from a different angle. I'd basically add a bool IsReadOnly property to my view model, Bind it to the relevant IsReadOnly properties on the UI controls and then simply set it to true of false dependant upon some UI interaction:

public bool IsReadOnly { get; set; } // Implement INotifyPropertyChanged here

...

<TextBox Grid.Row="0" IsReadOnly="{Binding IsReadOnly}" ... />
...
<TextBox Grid.Row="3" IsReadOnly="{Binding IsReadOnly}" ... />
<TextBox Grid.Row="4" ... /> <!--Never readonly-->

...

 IsReadOnly = true;
Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • I had considered this approach, but would not want to go there, since it requires to add IsReadOnly to each and every textbox (and IsEnabled to some others, etc.). Not desirable. – user1211286 Nov 18 '13 at 14:09
0

What I have used in similar scenarios is SetCurrentValue and InvalidateProperty:

This method is used by a component that programmatically sets the value of one of its own properties without disabling an application's declared use of the property. The SetCurrentValue method changes the effective value of the property, but existing triggers, data bindings, and styles will continue to work.

So, you can set IsReadOnly to true using SetCurrentValue, then later call InvalidateProperty to reset it to the "declared use" (which may be true or false).

However I'm not sure this is what you want. It sounds like you want the attached property to have precedence the entire time it's active. Using this method, if a binding updates sometime between SetCurrent and Invalidate, it would still be applied overriding the attached property.

Another half-solution is to add triggers. If this trigger was declared at the end of the TextBox style's Triggers collection, it would take precedence:

<DataTrigger Binding="{Binding GlobalIsReadonly}" Value="True">
    <Setter Property="IsReadOnly" Value="True" />
</DataTrigger>

The problem is that it only takes precedence over other setters (styles or triggers), but not direct setting of the property. Also styles cannot be modified after they are assigned, so to add this programmatically you would need to copy the old style and reassign it.

Unfortunately I think the only complete solution is to listen to property changes on all the textboxes and force another change.

nmclean
  • 7,564
  • 2
  • 28
  • 37