Silverlight AutoCompleteBox

by Bron Skinner 13. April 2010 11:58

In looking online it would appear there are already a couple AutoCompleteBox examples floating about - Example.

However, in my usual spirit I’ve decided to tackle the problem myself and have come up with (albeit rough, but only lacking tweaking), a pretty cool implementation of my own Silverlight AutoCompleteBox.

Silverlight AutoCompleteBox

An  AutoCompleteSearchBox class takes an ObservableCollection<T> of source items (a complete listing of items to search), and stores this as well as a PropertyPath with which to search by.

public class AutoCompleteSearchBox<T> : ObservableCollection<T>

    {

        private ObservableCollection<T> ItemSource { get; set; }

        private PropertyPath SearchProperty { get; set; }

        public AutoCompleteBox ACBox;

 

        public AutoCompleteSearchBox(ObservableCollection<T> itemSource, PropertyPath searchProperty)

        {

            SetItemSource(itemSource, searchProperty);

        }

An AutoCompleteBox class builds and returns a simple Grid containing a TextBlock and ListBox as well as attaching our various events and providing a means for updating the ListBox ItemsSource.

public class AutoCompleteBox

    {

        public TextBox SearchField;

        public ListBox ResultsField;

        public Grid LayoutRoot;

        public AutoCompleteResult SelectedResult = new AutoCompleteResult(string.Empty, -1);

You’ll notice a third type reference AutoCompleteResult. This is used to store any search matches in a predictable format and is the type our ListBox’s ItemTemplate is expecting when we set its ItemsSource.

public partial class AutoCompleteResult

    {

        public string Display { get; set; }

        public int Index { get; set; }

 

        public AutoCompleteResult(string display, int index)

        {

            Display = display;

            Index = index;

        }

    }

Our ResultsField (ListBox) which displays matches found uses a simple DataTemplate binding a single TextBlock’s Text to the Display Property of our AutoCompleteResult type.

  ResultsField = new ListBox();

            ResultsField.ItemTemplate = (DataTemplate)XamlReader.Load("<DataTemplate xmlns='http://schemas.microsoft.com/client/2007' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'><StackPanel><TextBlock Text='{Binding Path=Display, Mode=OneWay}' /></StackPanel></DataTemplate>");

            ResultsField.MouseEnter += new MouseEventHandler(ShowResultsField);

            ResultsField.MouseLeave += new MouseEventHandler(HideResultsField);

            ResultsField.MouseLeftButtonDown += new MouseButtonEventHandler(ResultsField_MouseLeftButtonDown);

            ResultsField.Visibility = Visibility.Collapsed;

Notice as well the MouseEvents which allow us to influence how our results show and hide as we MouseEnter, MouseLeave, KeyDown etc.

To enable our search filtering we attach a SearchField_TextChanged eventhandler to our TextBlock’s TextChanged event.

protected void SearchField_TextChanged(object sender, RoutedEventArgs e)

        {

            ACBox.SetResults(FilterResults(((TextBox)sender).Text));

        }

This sets our ResultsField (ListBox) ItemsSource to the found matches from our source items converted to AutoCompleteResults.

The Actual filtration process (FilterResults above) uses an extremely simple implementation of RegularExpressions to compare string matches in each of our source items against the initially supplied PropertyPath:

public List<AutoCompleteResult> FilterResults(string input)

        {

            List<AutoCompleteResult> Matches = new List<AutoCompleteResult>();

 

            try

            {

                string pat = input;

                Regex r = new Regex(pat, RegexOptions.IgnoreCase);

                for (int q = 0; q < ItemSource.Count; q++)

                {

                    PropertyInfo pi = ItemSource[q].GetType().GetProperty(SearchProperty.Path);

                    if (pi != null)

                    {

                        Match m = r.Match(pi.GetValue(ItemSource[q], null).ToString());

                        if (m.Success)

                        {

                            Matches.Add(new AutoCompleteResult(pi.GetValue(ItemSource[q], null).ToString(), q));

                        }

                    }

                }

            }

            catch(Exception ex)

            {

 

            }

            return Matches;

        }

Finally, we’ll want to do something with our selected result (typeof(T)). Our AutoCompleteBox’s SelectedResult property stores the last selected AutoCompleteResult from our ResultsField (ListBox.SelectedItem). A few checks to ensure that the SelectedResult isn’t outside the scope of our item source and vuala.

public T GetSelectedResult()

        {

            if (ACBox.SelectedResult != null && ACBox.SelectedResult.Index >= 0 && ACBox.SelectedResult.Index < ItemSource.Count)

            {

                return ItemSource[ACBox.SelectedResult.Index];

            }

            return default(T);

        }

Conclusions:

A pretty simplistic implemenation I'd like to see the ability to filter by non-string properties as well as supply custom regular expressions with which to filter against. I've not tested this control against extremely large lists and would like to, instead of looping through our source items, implement LINQ.

Tags:

Technical

Silverlight Pagination

by Bron Skinner 10. April 2010 03:44

I know this topic is a little run of the mill and you can probably find built in solutions for pagination within one of the latest Silverlight toolkits but I thought I’d take a minute to review a custom pagination solution for Silverlight 3.

 

PaginationHelper<object> phelper = new PaginationHelper<object>(new ObservableCollection<object>(), 10);

dg.ItemsSource = phelper;

dg.SelectedIndex = 0;

stackpanel_pagination.Children.Add(phelper.GetPaginationControl());

 

Without getting too fancy I’ve developed a PaginationHelper class that extends an ObservableCollection<T>. In this way we can use the class itself as the DataContext or ItemsSource for the FrameworkElement in question.

 

public class PaginationHelper<T> : ObservableCollection<T>

    {

        private ObservableCollection<T> ItemsRef { get; set; }

        private int ItemCount { get; set; }

        private int PageItemCount { get; set; }

        private int TotalPages { get; set; }

        ...

 

So our basic steps for setting up pagination for a control:

 

a)      Instantiate the PaginationHelper()

b)      Set the DataContext/ItemsSource for our control

c)       Generate/Add our Pagination Control

 

 

a) Instantiate the PaginationHelper()

 

PaginationHelper<object> phelper = new PaginationHelper<object>(new ObservableCollection<object>(), 10);

 

private ObservableCollection<T> ItemsRef { get; set; }

private int ItemCount { get; set; }

private int PageItemCount { get; set; }

private int TotalPages { get; set; }

private int CurrentPage { get; set; }

private int IndexDisplay = 5;

private int ActiveIndexDisplay { get; set;}

private StackPanel sp;

private double opacity = 0.5;

 

public PaginationHelper(ObservableCollection<T> items, int pageItemCount)

{

            ItemsRef = items;

            ItemCount = ItemsRef.Count();

            PageItemCount = pageItemCount;

TotalPages = Convert.ToInt32(Math.Ceiling((double)ItemCount / (double)PageItemCount));

            CurrentPage = -1;

            ActiveIndexDisplay = 1;

}

 

Our Properties:

 

ItemsRef – stores the original DataContext/ItemsSource for reference.

ItemCount – Total Items in the ItemsRef (could just use ItemsRef.Count()).

PageItemCount – Items per “Page” of data.

TotalPages – TotalPages required to paginate contents of ItemsRef as per PageItemCount.

CurrentPage – Current active page.

IndexDisplay – Number Page shortcuts to display (pagination control element)

ActiveIndexDisplay – Group index of active IndexDisplay.

Sp – Our pagination control element parent

Opacity -  non-focused control element buttons (faded)

 

b) Set the DataContext/ItemSrouce for our Control

 

dg.ItemsSource = phelper;

dg.SelectedIndex = 0;

 

This Step is pretty simple, as the PaginationHelper is an ObservableCollection<T> in itself it can serve as the DataContext/ItemSource for your control.

 

c) Set the DataContext/ItemSrouce for our Control

 

stackpanel_pagination.Children.Add(phelper.GetPaginationControl());

 

This step calls the GetPaginationControl() function which generates a number of buttons that each call and load a page of items, and is returned as a FrameworkElement (StackPanel). The page the btn represents is stored as an int in its Tag property. The returned FrameworkElement can be placed really wherever you want. I placed mine underneath the DataGrid I set the PaginationHelper up for.

 

Digitalboon Silverlight Pagination

 

public FrameworkElement GetPaginationControl()

        {

            sp = new StackPanel();

            sp.Height = 21;

            sp.Orientation = Orientation.Horizontal;

 

           

            for (int i = 1; i <= TotalPages; i++)

            {

                Button btn_page = new Button();

                btn_page.Name = "page" + i.ToString();

                btn_page.Opacity = opacity;

                btn_page.Content = i.ToString();

    btn_page.Style =    (Style)App.Current.Resources["ButtonTabStyle"];

                btn_page.Width = 17;

                btn_page.Height = 17;

                btn_page.Tag = i;

                btn_page.MouseEnter += (s, e) =>

                    {

                        btn_page.Cursor = Cursors.Hand;

                        btn_page.Opacity = 1.0;

                    };

                btn_page.MouseLeave += (s, e) =>

                    {

                        btn_page.Cursor = Cursors.Arrow;

                        if ((CurrentPage) != (int)btn_page.Tag)

                        {

                            btn_page.Opacity = opacity;

                        }

                    };

                btn_page.Click += new RoutedEventHandler(btn_page_Click);

                sp.Children.Add(btn_page);

 

                if (i > IndexDisplay)

                {

                    btn_page.Visibility = Visibility.Collapsed;

                }

            }

 

 

            GetPage(1);

 

            return sp;

        }

 

When a page button is clicked  the GetPage function is called which takes the clicked button’s tag (int) property, retrieves a list of items representing the requested “page” of data, and reassigns those items to the entire class itself. As the PaginationHelper class extends ObservableCollection<T> (see INotifyPropertyChanged), the displayed data updates itself.

 

It’s worth noting as well that using a back and forward button users can manually iterate page loads in an ascending/descending pattern, and that multiple pages of pages can be setup such that iterating beyond the scope of the current displayed collection of pages will load the next set of pages:

 

private void SetPaginationVisibility()

        {

            if(sp != null)

            {

                for (int i = 1; i <= TotalPages; i++)

                {

                    if (sp.Children[i].GetType() == typeof(Button))

                    {

                        Button btn = (Button)sp.Children[i];

                        if (btn.Tag != null)

                        {

                            int high = IndexDisplay * ActiveIndexDisplay;

                            int low = high - IndexDisplay;

                            if ((int)btn.Tag > low && (int)btn.Tag <= high)

                            {

                                btn.Visibility = Visibility.Visible;

                            }

                            else

                            {

                                btn.Visibility = Visibility.Collapsed;

                            }

                        }

                    }

                }

            }

        }

 

Conclusion

 

There are a few things that could be cleaned up, there may be better ways of implemented a pagination solution, but all in all this turned out to be a pretty simple working pagination helper.

Tags:

Technical

Powered by BlogEngine.NET 1.5.0.7
Custom theme for digitalboon.com by Bron Skinner

About the author

A Software Developer with a keen artistic sense, I’ve spent the last couple years working with predominantly Microsoft-based technologies developing web applications. The majority of this time has been spent building applications with SilverlightTM that forward some rather unique approaches to interface design. I am currently working full time.

 

 

Make sure to check out The Forge.


Download Resume