Silverlight Tags Control Using RichTextBox, Popup and ListBox.


 

Download Sample

 

 

Before couple of days I created a an editable tagging control with autocomplete list.
I decided to create one “some how” Smile similar to Live mail application “To” field.

to

That’s exactly what I need; RichTextBox, Popup Control, and a ListBox inside that popup, and SL Cinch MVVM Framework.

And one more thing InlineUIContainer that contains my Tag Item control.

InlineUIContainer object used to wrap FrameworkElement controls inside RichTextBox; so the only way to add (border, StackPanel, or even buttons) is by using this object.

Note:

Buttons will be disabled if RichTextBox ReadOnly property equal false.
So if you want to add delete button to your Tag Item; I think that you should to make your RichTextBox works in modes (Editable, ReadOnly).
Editable: to add inline tag items.
RedOnly: to click on delete buttons for each tag Item.

I did not like this kind of user experience. So I just delete delete button from tag item. Make things simpler that better Smile. You can just delete it using backspace or by highlighting some item then delete “As you saw in Video”.

 

When you download the sample, you will find a Silverlight library project called bunjeeb.SL.Controls

Three DLLs added to this project:

  • Microsoft.Expression.Interactions
  • System.Windows.Interactivity
  • Cinch.SL

You can find these three DLLs in dependencies folder with the sample.

As you see below TagTextBox is very simple:

<Grid x:Name="LayoutRoot">

<Popup x:Name="PopupTags" IsOpen="False">
<Grid>
<ListBox x:Name="PopupTagsList" Height="Auto" 
    MinWidth="200" MaxHeight="250"
    Background="White" 
    BorderBrush="Black" 
    KeyDown="PopupListBox_KeyDown"
    ItemContainerStyle="{StaticResource 
                          ListBoxItemContainerStyle}" 
    Padding="1,1,1,2"/>
</Grid>
</Popup>

<RichTextBox x:Name="rtb"
   AcceptsReturn="False"
   BorderBrush="{x:Null}" 
   Background="{x:Null}" BorderThickness="1,1,0,0">
   <i:Interaction.Triggers>
     <i:EventTrigger EventName="KeyUp">
      <Cinch:EventToCommandTrigger 
         Command="{Binding RichTextBoxKeyUp, 
                  ElementName=TagsTxtBox}"/>
     </i:EventTrigger>
     <i:EventTrigger EventName="KeyDown">
      <Cinch:EventToCommandTrigger 
         Command="{Binding RichTextBoxKeyDown,
                  ElementName=TagsTxtBox}"/>
     </i:EventTrigger>
     <i:EventTrigger EventName="ContentChanged">
      <Cinch:EventToCommandTrigger 
         Command="{Binding RichTextBoxContentChanged, 
                  ElementName=TagsTxtBox}"/>
     </i:EventTrigger>
     <i:EventTrigger EventName="LostFocus">
        <Cinch:EventToCommandTrigger 
         Command="{Binding RichTextBoxLostFocus, 
                  ElementName=TagsTxtBox}"/>
     </i:EventTrigger>
   </i:Interaction.Triggers>
</RichTextBox>

</Grid>

 

As you see here; I used EventToCommandTrigger to invoke these events using Commands. And that’s gonna help MVVMers to create there own ViewModel for that control. (If they want)

I’m sure that Sacha Barber (This one who creates Cinch Framework) used WeakReferences. and that gonna help GC to do his work. Smile Or simply you just use the delegate commands, or relay commands instead in PRISM. For me I prefer to use Cinch framework; I think it’s a preference.

TagsTextBox Control contains three properties:

  • Tags DependencyProperty: To display tags in RichTextBox.
  • AllTags DependencyProperty: Which contains all tags to display it on autocomplete popup.
  • InlineUITags CLR read only property: Which contains the Tag Items that you can find it in RichTextBox. 

When user starts typing FilterPopupList will be executed. The Main functionality of this Method is to:

  • Iterating into all InlineUIContainers to get tags texts.
  • Tags Popup will display = all tags except existing tags in RichTextBox.
private void FilterPopupList(string changedText)
{
   if (changedText == null)
   {
      PopupTagsList.ItemsSource = null;
      return;
   }

   //Get Tags from RichTextBox
   List<string> newTags = new List<string>();
   foreach (InlineUIContainer inlineUiContainer 
                in this.InlineUITags)
   {
     TextBlock tagNameTxtBlock = (TextBlock)((StackPanel)
      (((Border)inlineUiContainer.Child).Child)).Children[0];
            newTags.Add(tagNameTxtBlock.Text);
   }

   IEnumerable<string> tags = this.AllTags
                .Where(x => x.ToLower()
                         .Contains(changedText.ToLower()))
                                      .Except(newTags);

   PopupTagsList.ItemsSource = null;
   PopupTagsList.ItemsSource = tags;
}

 

When user commit tag insertion (by clicking enter or by choosing one of the items in the list); Then the source should be updated.

UpdateTagsPropertyAndBindingSource will do this work. Let me explain the logic
this method in three points.
  • Reset popup state if you want using isResetPopupState parameter.
  • Collecting the new added Tags.
  • Some Tags were deleted; so we should tell the source about it.
  • And Some tags were added; and we should tell the source about it also.
  • Update source using binding.UpdateSource()
  • And Finally, Raise TagsChanged event which contains Tags Added & Tags Deleted.

  • private void UpdateTagsPropertyAndBindingSource(
              bool isResetPopupState)
    {
          if (isResetPopupState)
             ResetPopupState();
    
          IEnumerable<string> newTags = GetNewItemsAdded();
    
          // If some item found in Tags Collection And 
          // not in RichTextBox; So we should delete it.
          IEnumerable<string> mustBeDeleted = 
                               this.Tags.Except(newTags);
          List<string> mustBeDeletedList = null;
    
          //Tags must be deleted from the source
          int mustBeDeletedCount = mustBeDeleted.Count();
          int lastRemoveIndex = mustBeDeletedCount - 1;
          if (mustBeDeletedCount > 0)
          {
             mustBeDeletedList = mustBeDeleted.ToList();
             for (int i = lastRemoveIndex; i >= 0; i--)
               this.Tags.Remove(mustBeDeletedList[i]);
          }
    
           //Tags must be added to the source
           IEnumerable<string> mustBeAdded = 
                                 newTags.Except(this.Tags);
           IEnumerable<string> mustBeAddedList = null;
           int mustBeAddedCount = mustBeAdded.Count();
           if (mustBeAddedCount > 0)
           {
             mustBeAddedList = mustBeAdded.ToList();
             foreach (string mustAddItem in mustBeAdded)
               this.Tags.Add(mustAddItem);
            }
    
            if (mustBeDeletedCount > 0 || mustBeAddedCount > 0)
            {
             BindingExpression binding = 
               this.GetBindingExpression(TagsTextBox.TagsProperty);
               if (null != binding) binding.UpdateSource();
    
               if (this.TagsChanged != null)
               {
                 TagsChanged(binding.DataItem, 
                   new TagsChangedArgs()
                 {
                    TagOwner = binding.DataItem,
                    TagsAdded = mustBeAddedList,
                    TagsDeleted = mustBeDeletedList
                 });
              }
         }
     }

Ok, One more thing to know, What If you have multiple instances of this control. And as you see in the video; that you can add entire new tag which is not listed in the auto complete popup list.

So how could tell other controls that All Tags List changed?

If sure that you folks have many many solutions in binding, Observable collection has the ability to notify the UI when item added. yeah you are right.

I decided to use the Mediator in Cinch framework. Its really amazing (pub sub mechanism) which is like Event Aggregator in PRISM.

So I just publish new All Tags List.

I just passed the Id to refocus RichTextBox.

[MediatorMessageSink(bunjeeb.SL.Common.MediatorMessages.
            TagsChangedMessage.TagsChanged)]
        public void CandidateTagsChangedMessage(
            Tuple<int, IEnumerable<string>, Type> tuple)
    {
       this.AllTags = tuple.Item2;

       PropertyInfo pinfo = tuple.Item3.GetProperty("Id");
       int currentId = (int)pinfo.GetValue(this.DataContext,
                                                null);
       int updatedId = tuple.Item1;

       if (updatedId == currentId)
           this.rtb.Focus();
  }

 

So the above code for the subscriber. And the publisher is our main view model.

public ICommand TagsChangedCommand { get; set; }
private void OnTagsChangedCommand(EventToCommandArgs args)
{
    TagsChangedArgs e = args.EventArgs as TagsChangedArgs;

    // TODO: Remove, add tags, Submit Changes to RIA Service
    // e.TagsAdded
    // e.TagsDeleted

    bunjeeb.SL.Common.MediatorMessages.TagsChangedMessage
       .Send(new Tuple<object>(e.TagOwner));
}

 

This command is binded with our TagsTextBox.TagsChanged Event

<bunjeebControls:TagsTextBox x:Name="MyTagsTextBox2"
   AllTags="{Binding AllTags}" 
   Tags="{Binding SomeTags2, Mode=TwoWay}" >

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="TagsChanged">
            <Cinch:EventToCommandTrigger 
        Command="{Binding TagsChangedCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

</bunjeebControls:TagsTextBox>

 

Sorry for not completing the 2nd article of pivot view control. I found that its more important to talk about Tagging Control. Since I did not any Silverlight Tagging Control.

That’s it. Wishing best coding in your life Smile

Download Sample

Advertisements