Ramblings of a .NET Developer

17 December 2013

Tuesday, December 17, 2013 | by Paul | Categories: , , , | No comments
I recently had an interesting requirement. Basically, users were allowed access to any part of the application, but only users that had the correct permissions were allowed to change data. Further to this, the level of control required was down to the field level.

So, I couldn't just disable the window and be on my merry way. Also, as it was field level permissions I had to ignore validation for the field when it was read-only.

After having a think about this I decided to write a custom UserControl that would accept any content. This would have an IsEditable property and additional data source properties to use when the control was not editable.

So, some XAML...


    
        
    
    
    
        
        
        
                
    


As you can, there is nothing too special with the control. Basically it's just a layout grid that contains a ContentControl and a readonly TextBox. The visibility of these are toggled based on the IsEditable property and I used a TextBox to allow the user to still be able to copy data. Now the code behind...
    /// 
    /// Interaction logic for EditableControl.xaml
    /// 
    public partial class EditableControl : UserControl
    {
        public EditableControl()
        {
            InitializeComponent();
            DataContextChanged += OnDataContextChanged;
            GotFocus += EditableControl_GotFocus;
        }

        void EditableControl_GotFocus(object sender, RoutedEventArgs e)
        {
            try
            {
                var isShiftDown = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);

                if (!isShiftDown)
                {
                    if (Child != null && content.Visibility == System.Windows.Visibility.Visible && !Child.IsFocused && !Child.IsKeyboardFocusWithin)
                    {
                        Child.Focus();
                    }
                    else if (readonlyTextbox.Visibility == System.Windows.Visibility.Visible)
                    {
                        readonlyTextbox.Focus();
                    }
                }
            }
            catch
            {
                //exception could be thrown in some scenarios where child control shows a popup or context menu
            }
        }

        private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            UpdateSecurity(PropertyName);
        }

        public FrameworkElement Child
        {
            get { return (FrameworkElement)GetValue(ChildProperty); }
            set { SetValue(ChildProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Child.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ChildProperty =
            DependencyProperty.Register("Child", typeof(FrameworkElement), typeof(EditableControl), new UIPropertyMetadata(null));

        public string PropertyName
        {
            get { return (string)GetValue(PropertyNameProperty); }
            set { SetValue(PropertyNameProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PropertyName.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PropertyNameProperty =
            DependencyProperty.Register("PropertyName", typeof(string), typeof(EditableControl), new FrameworkPropertyMetadata(OnPropertyNameChanged));

        private static void OnPropertyNameChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var control = sender as EditableControl;
            var context = control.DataContext as ValidatingViewModel;
            if (e.NewValue == null)
                control.UpdateSecurity(string.Empty);
            else
                control.UpdateSecurity(e.NewValue.ToString());
        }

        private void UpdateSecurity(string name)
        {
            var context = DataContext as ViewModel;
            if (context == null)
            {
                Logger.Warn("DataContext not set");
            }
            else if (!string.IsNullOrEmpty(name))
            {
                IsEditable = context.CanEdit(name);
            }
        }

        public bool IsEditable
        {
            get { return (bool)GetValue(IsEditableProperty); }
            set { SetValue(IsEditableProperty, value); }
        }

        // Using a DependencyProperty as the backing store for IsEditable.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsEditableProperty =
            DependencyProperty.Register("IsEditable", typeof(bool), typeof(EditableControl), new UIPropertyMetadata(true, OnIsEditableChanged));

        private static void OnIsEditableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var control = (RossEditableControl)sender;
            control.UpdateSecurity(control.PropertyName);
        }

        public string ReadonlySource
        {
            get { return (string)GetValue(ReadonlySourceProperty); }
            set { SetValue(ReadonlySourceProperty, value); }
        }

     public TextWrapping TextWrapping
     {
   get { return (TextWrapping)GetValue(TextWrappingProperty); }
   set { SetValue(TextWrappingProperty, value); }
     }

        // Using a DependencyProperty as the backing store for ReadonlySource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ReadonlySourceProperty =
            DependencyProperty.Register("ReadonlySource", typeof(string), typeof(EditableControl), new UIPropertyMetadata(null));

  public static readonly DependencyProperty TextWrappingProperty =
     DependencyProperty.Register("TextWrapping", typeof(TextWrapping), typeof(EditableControl), new UIPropertyMetadata(null));
    }
Here I've created DependencyProperties for Child, PropertyName, IsEditable, ReadonlySource and TextWrapping.
These allow enough configuration to allow us to customize how the readonly field is will be displayed. Also, I've not shown it here, but I have a style for the TextBox that will render the text in a gray italic font and gray border when it is read-only.
For the styling of the application, this was a good fit aesthetically.
I have a base ViewModel that exposes a virtual CanEdit function. This is used in the UpdateSecurity method to determine if the control should be readonly or not. This method is called when the PropertyName property changes.
For many scenarios this will work fine (i.e. the IsEditable set when the PropertyName gets set), but in some cases you will need it to be more dynamic. So, the IsEditable property could be bound to some property on your ViewModel.
Below is an example of how it would be used.

    
        
    


0 comments:

Post a Comment