Auto Lock Class
I wrote the "AutomaticLock" class, which has the inherited "DoLock" property attached.
Setting the "DoLock" property to true re-templates for all text fields of ComboBoxes, CheckBoxes, etc., to be text blocks, not editable CheckBoxes, etc. My code is set up so that another attached property can specify an arbitrary pattern for use in blocking mode ("view"), controls that should never be automatically locked, etc.
Thus, the same view can be easily used both for editing and for viewing. Setting one property changes it back and forth, and it is fully customizable, since any control in the view can run the DoLock property to change its appearance or behavior arbitrarily.
Implementation code
Here is the code:
public class AutomaticLock : DependencyObject { Control _target; ControlTemplate _originalTemplate; // AutomaticLock.Enabled: Set true on individual controls to enable locking functionality on that control public static bool GetEnabled(DependencyObject obj) { return (bool)obj.GetValue(EnabledProperty); } public static void SetEnabled(DependencyObject obj, bool value) { obj.SetValue(EnabledProperty, value); } public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached("Enabled", typeof(bool), typeof(AutomaticLock), new FrameworkPropertyMetadata { PropertyChangedCallback = OnLockingStateChanged, }); // AutomaticLock.LockTemplate: Set to a custom ControlTemplate to be used when control is locked public static ControlTemplate GetLockTemplate(DependencyObject obj) { return (ControlTemplate)obj.GetValue(LockTemplateProperty); } public static void SetLockTemplate(DependencyObject obj, ControlTemplate value) { obj.SetValue(LockTemplateProperty, value); } public static readonly DependencyProperty LockTemplateProperty = DependencyProperty.RegisterAttached("LockTemplate", typeof(ControlTemplate), typeof(AutomaticLock), new FrameworkPropertyMetadata { PropertyChangedCallback = OnLockingStateChanged, }); // AutomaticLock.DoLock: Set on container to cause all children with AutomaticLock.Enabled to lock public static bool GetDoLock(DependencyObject obj) { return (bool)obj.GetValue(DoLockProperty); } public static void SetDoLock(DependencyObject obj, bool value) { obj.SetValue(DoLockProperty, value); } public static readonly DependencyProperty DoLockProperty = DependencyProperty.RegisterAttached("DoLock", typeof(bool), typeof(ControlTemplate), new FrameworkPropertyMetadata { Inherits = true, PropertyChangedCallback = OnLockingStateChanged, }); // CurrentLock: Used internally to maintain lock state [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public static AutomaticLock GetCurrentLock(DependencyObject obj) { return (AutomaticLock)obj.GetValue(CurrentLockProperty); } public static void SetCurrentLock(DependencyObject obj, AutomaticLock value) { obj.SetValue(CurrentLockProperty, value); } public static readonly DependencyProperty CurrentLockProperty = DependencyProperty.RegisterAttached("CurrentLock", typeof(AutomaticLock), typeof(AutomaticLock)); static void OnLockingStateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e) { AutomaticLock current = GetCurrentLock(obj); bool shouldLock = GetDoLock(obj) && (GetEnabled(obj) || GetLockTemplate(obj)!=null); if(shouldLock && current==null) { if(!(obj is Control)) throw new InvalidOperationException("AutomaticLock can only be used on objects derived from Control"); new AutomaticLock((Control)obj).Attach(); } else if(!shouldLock && current!=null) current.Detach(); } AutomaticLock(Control target) { _target = target; } void Attach() { _originalTemplate = _target.Template; _target.Template = GetLockTemplate(_target) ?? SelectDefaultLockTemplate(); SetCurrentLock(_target, this); } void Detach() { _target.Template = _originalTemplate; _originalTemplate = null; SetCurrentLock(_target, null); } ControlTemplate SelectDefaultLockTemplate() { for(Type type = _target.GetType(); type!=typeof(object); type = type.BaseType) { ControlTemplate result = _target.TryFindResource(new ComponentResourceKey(type, "AutomaticLockTemplate")) as ControlTemplate ?? _target.TryFindResource(new ComponentResourceKey(typeof(AutomaticLock), type.Name)) as ControlTemplate; if(result!=null) return result; } return null; } }
This code will allow you to specify an automatic lock pattern based on controls individually or it will allow you to use the default patterns defined either in the assembly containing the AutomaticLock class in the assembly containing your custom control, which lock pattern applies to your local resources in your visual tree (including application resources)
How to identify AutomaticLock patterns
The default templates for standard WPF controls are defined in the assembly containing the AutomaticLock class in the ResourceDictionary, combined with Themes / Generic.xaml. For example, this pattern causes all TextBoxs to turn into TextBlocks when locked:
<ControlTemplate TargetType="{x:Type TextBox}" x:Key="{ComponentResourceKey ResourceId=TextBox, TypeInTargetAssembly={x:Type lc:AutomaticLock}}"> <TextBlock Text="{TemplateBinding Text}" /> </ControlTemplate>
The default templates for custom controls can be defined in the assembly containing the user control in the ResourceDictionary, placed in its Themes / Generic.xaml. In this case, the ComponentResourceKey component is different, for example:
<ControlTemplate TargetType="{x:Type prefix:MyType}" x:Key="{ComponentResourceKey ResourceId=AutomaticLockTemplate, TypeInTargetAssembly={x:Type prefix:MyType}}"> ...
If an application wants to override the default AutomaticLock pattern for a specific type, it can put the auto-lock pattern in its App.xaml, Window XAML, UserControl XAML, or in the ResourceDictionary of a separate control. In each case, the ComponentResourceKey should be specified in the same way as for custom controls:
x:Key="{ComponentResourceKey ResourceId=AutomaticLockTemplate, TypeInTargetAssembly={x:Type prefix:MyType}}"
Finally, an automatic lock pattern can be applied to a single control by setting its AutomaticLock.LockTemplate property.
How to use AutomaticLock in the user interface
To use auto-lock:
- Set AutomaticLock.Enabled = "True" to any controls that should be automatically locked. This can be done in style or directly on individual controls. It allows you to lock the control, but does not cause the control to lock.
- If you want to lock, set AutomaticLock.DoLock = "True" to your top-level control (Window, view, UserControl, etc.) when you want automatic locking to actually be performed. You can bind AutomaticLock.DoLock to a checkbox or menu item, or you can control it in code.
Some tips for effectively switching viewing and editing modes
This AutomaticLock class is great for switching between viewing and editing modes, even if they are significantly different. I have several different methods for constructing my views in order to accommodate differences in layout during editing. Some of them:
Make controls invisible in edit or view mode by setting either their template or AutomaticLockTemplate to an empty template, depending on the situation. For example, suppose that "Age" is at the top of your layout in view mode and at the bottom in edit mode. Add a TextBox for "Age" in both places. An empty template is installed in the top template set, so it is not displayed in edit mode. At the bottom, set AutomaticLockTemplate to an empty template. Now only one will be displayed.
Use ContentControl to replace borders, layout panels, buttons, etc. without affecting the content. The ContentControl template has surrounding borders, panels, buttons, etc. For edit mode. It also has an AutomaticLockTemplate, which has a view mode version.
Use the control to replace the rectangular section of your view. (By this, I really mean the object of the Control class, not a subclass of therof.) Again, you put the version of the edit mode in the template and the version of the view in AutomaticLockTemplate.
Use a grid with extra rows and columns with automatic size. Use the trigger of the AutomaticLock.DoLock property to update the Row, Column, RowSpan and ColumnSpan properties of elements in the Grid. For example, you can move the panel containing the Age control at the top by changing its Grid.Row value from 6 to 0.
A DoLock trigger to apply a LayoutTranform or RenderTransform to your elements or to set other properties, such as Width and Height. This is useful if you want things to be bigger in edit mode, or if you want to make the TextBox wider and move the button next to it relative to the edge.
Please note that you can use option No. 3 (control object with separate templates for editing and viewing modes) for the entire view. This would be done if the editing and viewing modes were completely different. In this case, AutomaticLock still gives you the opportunity to install two templates manually. It will look like this:
<Control> <Control.Template> <ControlTemplate> </ControlTemplate> </Control.Template> <lib:AutomaticLock.LockTemplate> <ControlTemplate> </ControlTemplate> </lib:AutomaticLock.LockTemplate> </Control>
It is usually easier to set up a few small positions and things between the editing and viewing modes and is better for your user, because the user will have a consistent layout, but if you need a complete replacement, then AutomaticLock gives you such power as well.