пятница, 23 декабря 2011 г.

ItemSelector на базе AutoCompliteBox

У AutoCompliteBox есть существенный недостаток. Он предполагает (AutoCompliteBox, а не недостаток), что все данные в него уже загружены и осталось только выбрать. К сожалению в Silverlight при использовании RIA сервисов возникает проблема с объемом передаваемых данных. См. здесь.
Стоит задача, сделать компонент по функционалу похожий на AutoCompliteBox, но подразумевающий, что данные в него грузятся только после того, как пользователь введет часть названия объекта который ему необходим.
Т.к. контрол предполагается использовать во внешнем мире, то от него требуется в этот самый внешний мир предоставить:
1. Поле для хранения выбранного элемента.
/// 
/// Выбранный элемент
/// 
public Entity SelectedItem
{
 get { return (Entity)GetValue(SelectedItemProperty); }
 set { SetValue(SelectedItemProperty, value); }
}
 
/// 
/// Static part of dependency property SelectedItem
/// 
public static readonly DependencyProperty SelectedItemProperty =
 DependencyProperty.Register("SelectedItem"typeof(Entity), typeof(ItemSelector), new PropertyMetadata(null, SelectedItem_Changed));
2. Поле с коллекцией объектов из которых выбирать.
/// 
/// Коллекция отфильтрованных обхектов
/// 
public IEnumerable<Entity> FilterdItems
{
 get { return (IEnumerable<Entity>)GetValue(FilterdItemsProperty); }
 set { SetValue(FilterdItemsProperty, value); }
}
 
/// 
/// Static part of dependency property FilterdItems
/// 
public static readonly DependencyProperty FilterdItemsProperty =
  DependencyProperty.Register("FilterdItems"typeof(IEnumerable<Entity>), typeof(ItemSelector), new PropertyMetadata(null, FilterdItems_Changed));
3. Команду, о том, что пользователь уже соизволил повводить данные и пора бы уже поискать.
/// 
/// Команда для инициации поиска
/// 
public ICommand FindItemsCommand
{
 get { return (ICommand)GetValue(FindItemsCommandProperty); }
 set { SetValue(FindItemsCommandProperty, value); }
}
 
/// 
/// Static part of dependency property FindItems
/// 
public static readonly DependencyProperty FindItemsCommandProperty =
 DependencyProperty.Register("FindItemsCommand"typeof(ICommand), typeof(ItemSelector), new PropertyMetadata(null));
Ну интерфейс тоже получается незамысловатый:
<Grid x:Name="LayoutRoot" >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="30" />
    Grid.ColumnDefinitions>
    <sdk:AutoCompleteBox VerticalAlignment="Center" x:Name="acbFilter" TextBoxStyle="{StaticResource AligmentStyle}"
      ItemsSource="{Binding Path=FilterdItems, ElementName=mainControl}" 
      SelectedItem="{Binding Path=SelectedItem, ElementName=mainControl,Mode=TwoWay}"
                             
                            />
    <Button x:Name="btnSearch" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
        <Image Source="./../Images/search.png" Width="20" />
    Button>
Grid>
Теперь как это все работает.
Как видим в основе у нас лежит все тот же  AutoCompliteBox. Его поля привязаны к вышеописанным полям.
Идея такая: пользователь вводит несколько буковок и нажимает на кнопку или клавишу Enter. Во внешний мир уходит команда, по которой грузятся данные и попадают в свойство Filtereditems. А уже из них пользователь выбирает то. что ему нужно. Повторные нажатия кнопки и Enter, также вызывают запросы на обновление данных.
Что нужно еще, чтобы это заработало? На самом деле не так уж и много.
Для начала подписываем всех заинтерисованных на всякие разные события:

void ItemSelector_Loaded(object sender, RoutedEventArgs e)
{
 acbFilter.KeyUp += new KeyEventHandler(acbFilter_KeyUp);
 acbFilter.SelectionChanged += new SelectionChangedEventHandler(acbFilter_SelectionChanged);
 acbFilter.LostFocus += new RoutedEventHandler(acbFilter_LostFocus);
 btnSearch.Click += new RoutedEventHandler(Button_Click);
}

Собственно клик на кнопке, вызывает команду передав туда в качестве параметра текст из AutoCommpliteBox-а:

protected void Button_Click(object sender, RoutedEventArgs e)
{
 if (FindItemsCommand != null && FindItemsCommand is DelegateCommand<string> && SelectedItem == null)
 {
  acbFilter.IsEnabled = false;
  (FindItemsCommand as DelegateCommand<string>).Execute(acbFilter.Text);
 
 }
}

Обработчик KeyUp делает тоже самое, но только для клавиши Enter:

private void acbFilter_KeyUp(object sender, KeyEventArgs e)
{
 if (e.Key == Key.Enter)
 {
  Button_Click(sender, null);
 }
}

Ну а если нам уже вернули список элементов, то проверяем сколько там чего, если 1 - сразу выбрать, если много, развернуть список, если нет, сказать что пользователь неправ:

private static void FilterdItems_Changed(object sender, DependencyPropertyChangedEventArgs e)
{
 IEnumerable<Entity> items = e.NewValue as IEnumerable<Entity>;
 ItemSelector current = (sender as ItemSelector);
 current.acbFilter.IsEnabled = true;
 if (items != null)
 {
  if (items.Count() == 1)
  {
   current.SelectedItem = items.First();   
  }
  else if (items.Count() > 1)
  {
 
   current.acbFilter.Focus();
   current.Dispatcher.BeginInvoke(() => { current.acbFilter.IsDropDownOpen = true; });
  }
  else if (items.Count() == 0)
  {
   current.acbFilter.Background = (Brush)Application.Current.Resources["NotFoundBrush"];
  }
 }
}

Ну и напоследок, при изменении выбранного элемента порадуем пользователя подсветкой:

private static void SelectedItem_Changed(object sender, DependencyPropertyChangedEventArgs e)
{   
 ItemSelector current = sender as ItemSelector;
 if (e.NewValue == null)
 {
  current.acbFilter.Background = null;
 }
 else
 {
  current.acbFilter.Background = (Brush)Application.Current.Resources["FoundBrush"];
 }
}

Собственно все. Ну пара картинок как это работает:
Вводим несколько буковок и нажимаем Enter:

При вводе такой строки, которая позволяет выбрать только один объект:
и нажатии Enter:
Ну теперь точно - все.

Комментариев нет:

Отправить комментарий