Attaching tags to any entity in a generic way.

Introduction

I like the concept of tags as a means of categorizing things. It’s very flexible and you can take unions, intersections, etc.

It’s quite simple in lightswitch to foresee a generic approach, which is perfectly reusable in many other situations.

The ingredients are:

  • generics,
  • implementing an interface,
  • the dynamic keyword and
  • extension methods.
The approach is completely inspired by the work of Kostas Christodoulou (http://code.msdn.microsoft.com/silverlight/Managing-Custom-AddEdit-5772ab80). In his approach, a modal window is used for adding and editing an entity. Here we use a similar approach for many-to-many handling. To make things not too generic, I focus here on tagging, as an example of many-to-many handling.
The source of my solution is here: GenericTaggingModalWindow

The User Interface

Of course we need a screen for managing tags, but it’s not worth providing a screenshot. You just need a ListDetail screen for a table having one field (tagname).

So, let’s focus on the customer detail screen which contains a serialized list of assigned tags.

I agree that my representation of the assigned tags is rather poor. It’s clear the kind of custom control (a colored stackpanel) could improve the “user interface experience”.

So, the user can click on the “Edit Tags” button, which opens following modal screen:

 

You’ll find many examples of this approach. The screen has basically 2 grids: Assigned tags and available tags. The 2 buttons in the middle allow to assign or unassign tags. Of course, we try to leverage all the comfort that Silverlight is offering here:

  • The command pattern (the Execute and CanExecute methods) allows us to disable the buttons when appropriate.
  • The full duplex databinding makes sure that even the AssignedTag list on the main customer detail screen is always in sync with the situation on the picker screen.

Data Model

Let’s stick to the simple example of a customer table on which we apply tagging. So conceptually, each customer has an assigned list of tags. Tags can be anything: points of interest, participation in holidays, etc. …

First of all we need a Tag table:

Of course, we have our Customer table:

The customer table has a computed field named AssignedTagList, which is in fact the serialized representation of the next table, the AssignedTags Table. The code for the field computation is very simple:

 public partial class Customer
    {
        partial void AssignedTagList_Compute(ref string result)
        {
            if (this.AssignedTags != null)
            {
                result = string.Join<AssignedTag>(" | ", this.AssignedTags);
            }
        }
    }

The AssignedTag table goes as follows:

The AssignedTag table has also a computed field: TagName.

public partial class AssignedTag

    {
        partial void TagName_Compute(ref string result)
        {
            if (this.Tag!=null)
            {
                result = this.Tag.TagName;
            }
        }
    }

 

The “base infrastructure”

The code in the customer detail screen can be kept very simple, because I use a technique that I borrowed from Kostas.

In fact we implement an interface called IScreenWithAvailailablTags:

public interface IScreenWithAvailableTags<TEntity, TDetails, SEntity, SDetails> : IScreenObject
        where TEntity : EntityObject<TEntity, TDetails>
        where SEntity : EntityObject<SEntity, SDetails>
        where SDetails : EntityDetails<SEntity, SDetails>, new()
        where TDetails : EntityDetails<TEntity, TDetails>, new()
    {
        VisualCollection<TEntity> TagList { get; }

        VisualCollection<SEntity> AssignedTagList { get; }

        string TagFieldName { get; }
    }

This interface makes sure that our detail screen will have 2 VisualCollections (one for the TagList and one for the AssignedTagList. Furthermore it makes sure that we have a name for the field in the AssignedTagList refering to the Tag table. By doing so, this approach can be used for any many to many screen.

The interface definition is the base for a set of extension methods which will basically do the handling of the assignment and un-assignment of tags in our final customer details screen.

 

 public static class TagExtensions
    {

        public static void DoAssignThisTag<TEntity, TDetails, SEntity, SDetails>(this IScreenWithAvailableTags<TEntity, TDetails, SEntity, SDetails> screen)
            where TEntity : EntityObject<TEntity, TDetails>
            where TDetails : EntityDetails<TEntity, TDetails>, new()
            where SEntity : EntityObject<SEntity, SDetails>
            where SDetails : EntityDetails<SEntity, SDetails>, new()
        {
            if (screen.TagList.SelectedItem != null)
            {
                dynamic newAssignedTag = screen.AssignedTagList.AddNew();
                newAssignedTag.Details.Properties[screen.TagFieldName].Value = screen.TagList.SelectedItem; //Tag property is dynamically assigned !
                MoveToNextItemInCollection<TEntity>(screen.TagList);
            }
        }

        public static bool DoAssignThisTag_CanExecute<TEntity, TDetails, SEntity, SDetails>(this IScreenWithAvailableTags<TEntity, TDetails, SEntity, SDetails> screen)
            where TEntity : EntityObject<TEntity, TDetails>
            where TDetails : EntityDetails<TEntity, TDetails>, new()
            where SEntity : EntityObject<SEntity, SDetails>
            where SDetails : EntityDetails<SEntity, SDetails>, new()
        {
            bool result;
            if (screen.TagList.SelectedItem != null)
            {
                result = !(screen.AssignedTagList.Where(at =>
                {
                    dynamic at2 = at as dynamic;
                    return at2.Details.Properties[screen.TagFieldName].Value == screen.TagList.SelectedItem;
                }).Any()
                    );
            }
            else
            {
                result = false;
            }
            return result;
        }

        public static void DoUnAssignThisTag<TEntity, TDetails, SEntity, SDetails>(this IScreenWithAvailableTags<TEntity, TDetails, SEntity, SDetails> screen)
            where TEntity : EntityObject<TEntity, TDetails>
            where TDetails : EntityDetails<TEntity, TDetails>, new()
            where SEntity : EntityObject<SEntity, SDetails>
            where SDetails : EntityDetails<SEntity, SDetails>, new()
        {
            if (screen.AssignedTagList.SelectedItem != null)
            {
                if (screen.AssignedTagList.SelectedItem.Details.EntityState != EntityState.Deleted)
                {
                    screen.AssignedTagList.DeleteSelected();
                    screen.AssignedTagList.Refresh();
                }
            }
        }

        public static bool DoUnAssignThisTag_CanExecute<TEntity, TDetails, SEntity, SDetails>(this IScreenWithAvailableTags<TEntity, TDetails, SEntity, SDetails> screen)
            where TEntity : EntityObject<TEntity, TDetails>
            where TDetails : EntityDetails<TEntity, TDetails>, new()
            where SEntity : EntityObject<SEntity, SDetails>
            where SDetails : EntityDetails<SEntity, SDetails>, new()
        {
            bool result;

            if (screen.AssignedTagList.SelectedItem != null)
            {
                result = screen.AssignedTagList.SelectedItem.Details.EntityState != EntityState.Deleted;
            }
            else
            {
                result = false;
            }
            return result;
        }

        private static void MoveToNextItemInCollection<T>(VisualCollection<T> collection) where T : class, IEntityObject
        {
            if (!collection.SelectedItem.Equals(collection.Last()))
            {
                int currentIndex = collection.ToList().IndexOf(collection.SelectedItem);
                T nextElementInCollection = collection.ToList()[currentIndex + 1];
                if (nextElementInCollection != null)
                {
                    collection.SelectedItem = nextElementInCollection;
                }
            }
        }
    }

The customer detail screen code handling

Both the interface and the extension method make that the customerdetail screen handling is very straightforward

    public partial class CustomerDetail : IScreenWithAvailableTags<Tag, Tag.DetailsClass, AssignedTag, AssignedTag.DetailsClass>
    {
        private string _modalWindowControlName = "PickerModalWindow";

        partial void Customer_Loaded(bool succeeded)
        {
            this.SetDisplayNameFromEntity(this.Customer);
        }

        partial void Customer_Changed()
        {
            this.SetDisplayNameFromEntity(this.Customer);
        }

        partial void CustomerDetail_Saved()
        {
            this.SetDisplayNameFromEntity(this.Customer);
        }

        #region Tag (un)assignment button handling
        partial void AssignThisTag_Execute()
        {
            this.DoAssignThisTag();
        }

        partial void AssignThisTag_CanExecute(ref bool result)
        {
            result = this.DoAssignThisTag_CanExecute();
        }

        partial void UnassignThisTag_Execute()
        {
            this.DoUnAssignThisTag();
        }

        partial void UnassignThisTag_CanExecute(ref bool result)
        {
            result = this.DoUnAssignThisTag_CanExecute();
        }
        #endregion

        partial void EditTags_Execute()
        {

            this.OpenModalWindow(_modalWindowControlName);
        }

        #region IScreenWithAvailableTags implementation
        public VisualCollection<Tag> TagList
        {
            get { return this.AvailableTags; }
        }

        public VisualCollection<AssignedTag> AssignedTagList
        {
            get { return this.AssignedTags; }
        }

        public string TagFieldName
        {
            get { return "Tag"; }
        }
        #endregion
    }
}

Using the attached solution

The source of my solution is here: GenericTaggingModalWindow.

The source can be used directly, but contains no database, so you have to create some sample data. Create first some tags in the Tag management screen and create a customer.

Conclusion

LightSwitch allows to design screens in a very simple way. Nonetheless, for some screen designs, it can be useful to foresee kind of base infrastructure, like the one shown over here.