Before couple of days I created a an editable tagging control with autocomplete list.
I decided to create one “some how”
similar to Live mail application “To” field.
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. I did not like this kind of user experience. So I just delete delete button from tag item. Make things simpler that better |
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.
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 ![]()