Define a locking policy to create read-only segments
The Immutability API of the Visual Studio Visualization and Modeling SDK allows a program to lock part or all of a domain-specific language (DSL) model so that it can be read but not changed. This read-only option could be used, for example, so that a user can ask colleagues to annotate and review a DSL model but can disallow them from changing the original.
In addition, as author of a DSL, you can define a locking policy. A locking policy defines which locks are permitted, not permitted, or mandatory. For example, when you publish a DSL, you can encourage third-party developers to extend it with new commands. But you could also use a locking policy to prevent them from altering the read-only status of specified parts of the model.
Note
A locking policy can be circumvented by using reflection. It provides a clear boundary for third-party developers, but does not provide strong security.
More information and samples are available at Visual Studio Visualization and Modeling SDK.
Note
The Text Template Transformation component is automatically installed as part of the Visual Studio extension development workload. You can also install it from the Individual components tab of Visual Studio Installer, under the SDKs, libraries, and frameworks category. Install the Modeling SDK component from the Individual components tab.
Setting and getting locks
You can set locks on the store, on a partition, or on an individual element. For example, this statement prevents a model element from being deleted, and also prevents its properties from being changed:
using Microsoft.VisualStudio.Modeling.Immutability; ...
element.SetLocks(Locks.Delete | Locks.Property);
Other lock values can be used to prevent changes in relationships, element creation, movement between partitions, and reordering links in a role.
The locks apply both to user actions and to program code. If program code attempts to make a change, an InvalidOperationException
is thrown. Locks are ignored in an Undo or Redo operation.
You can discover whether an element has a lock in a given set by using IsLocked(Locks)
and you can obtain the current set of locks on an element by using GetLocks()
.
You can set a lock without using a transaction. The lock database isn't part of the store. If you set a lock in response to a change of a value in the store, for example in OnValueChanged
, you should allow changes that are part of an Undo operation.
These methods are extension methods that are defined in the Microsoft.VisualStudio.Modeling.Immutability namespace.
Locks on partitions and stores
Locks can also be applied to partitions and the store. A lock that is set on a partition applies to all the elements in the partition. Therefore, for example, the following statement prevents all the elements in a partition from being deleted, irrespective of the states of their own locks. Nevertheless, other locks such as Locks.Property
could still be set on individual elements:
partition.SetLocks(Locks.Delete);
A lock that is set on the store applies to all its elements, irrespective of the settings of that lock on the partitions and the elements.
Using locks
You could use locks to implement schemes such as the following examples:
Disallow changes to all elements and relationships except the ones that represent comments. This approach allows users to annotate a model without changing it.
Disallow changes in the default partition, but allow changes in the diagram partition. The user can rearrange the diagram, but can't alter the underlying model.
Disallow changes to the store except for a group of users who are registered in a separate database. For other users, the diagram and model are read-only.
Disallow changes to the model if a Boolean property of the diagram is set to true. Provide a menu command to change that property. This approach helps ensure users that they don't make changes accidentally.
Disallow addition and deletion of elements and relationships of particular classes, but allow property changes. This approach provides users with a fixed form in which they can fill the properties.
Lock values
Locks can be set on a store, partition, or individual ModelElement. Locks is a Flags
enumeration: you can combine its values using '|'.
Locks of a ModelElement always include the Locks of its partition.
Locks of a partition always include the Locks of the store.
You can't set a lock on a partition or store and at the same time disable the lock on an individual element.
Value | Meaning if IsLocked(Value) is true |
---|---|
None | No restriction. |
Property | Domain properties of elements can't be changed. This value doesn't apply to properties that are generated by the role of a domain class in a relationship. |
Add | New elements and links can't be created in a partition or store. Not applicable to ModelElement . |
Move | Element can't be moved between partitions if element.IsLocked(Move) is true, or if targetPartition.IsLocked(Move) is true. |
Delete | An element can't be deleted if this lock is set on the element itself, or on any of the elements to which deletion would propagate, such as embedded elements and shapes. You can use element.CanDelete() to discover whether an element can be deleted. |
Reorder | The ordering of links at a role player can't be changed. |
RolePlayer | The set of links that are sourced at this element can't be changed. For example, new elements can't be embedded under this element. This value doesn't affect links for which this element is the target. If this element is a link, its source and target aren't affected. |
All | Bitwise OR of the other values. |
Locking policies
As the author of a DSL, you can define a locking policy. A locking policy moderates the operation of SetLocks()
, so that you can prevent specific locks from being set or mandate that specific locks must be set. Typically, you would use a locking policy to discourage users or developers from accidentally contravening the intended use of a DSL, in the same manner that you can declare a variable private
.
You can also use a locking policy to set locks on all elements dependent on the element's type. This behavior is because SetLocks(Locks.None)
is always called when an element is first created or deserialized from file.
However, you can't use a policy to vary the locks on an element during its life. To achieve that effect, you should use calls to SetLocks()
.
To define a locking policy:
Create a class that implements ILockingPolicy.
Add this class to the services that are available through the DocData of your DSL.
To define a locking policy
ILockingPolicy has the following definition:
public interface ILockingPolicy
{
Locks RefineLocks(ModelElement element, Locks proposedLocks);
Locks RefineLocks(Partition partition, Locks proposedLocks);
Locks RefineLocks(Store store, Locks proposedLocks);
}
These methods are called when a call is made to SetLocks()
on a store, partition, or ModelElement. In each method, you're provided with a proposed set of locks. You can return the proposed set, or you can add and subtract locks.
For example:
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Immutability;
namespace Company.YourDsl.DslPackage // Change
{
public class MyLockingPolicy : ILockingPolicy
{
/// <summary>
/// Moderate SetLocks(this ModelElement target, Locks locks)
/// </summary>
/// <param name="element">target</param>
/// <param name="proposedLocks">locks</param>
/// <returns></returns>
public Locks RefineLocks(ModelElement element, Locks proposedLocks)
{
// In my policy, users can never delete an element,
// and other developers cannot easily change that:
return proposedLocks | Locks.Delete);
}
public Locks RefineLocks(Store store, Locks proposedLocks)
{
// Only one user can change this model:
return Environment.UserName == "aUser"
? proposedLocks : Locks.All;
}
To make sure that users can always delete elements, even if other code calls SetLocks(Lock.Delete):
return proposedLocks & (Locks.All ^ Locks.Delete);
To disallow change in all the properties of every element of MyClass:
return element is MyClass ? (proposedLocks | Locks.Property) : proposedLocks;
To make your policy available as a service
In your DslPackage
project, add a new file that contains code that resembles the following example:
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Immutability;
namespace Company.YourDsl.DslPackage // Change
{
// Override the DocData GetService() for this DSL.
internal partial class YourDslDocData // Change
{
/// <summary>
/// Custom locking policy cache.
/// </summary>
private ILockingPolicy myLockingPolicy = null;
/// <summary>
/// Called when a service is requested.
/// </summary>
/// <param name="serviceType">Service requested</param>
/// <returns>Service implementation</returns>
public override object GetService(System.Type serviceType)
{
if (serviceType == typeof(SLockingPolicy)
|| serviceType == typeof(ILockingPolicy))
{
if (myLockingPolicy == null)
{
myLockingPolicy = new MyLockingPolicy();
}
return myLockingPolicy;
}
// Request is for some other service.
return base.GetService(serviceType);
}
}