0

I have a custom control LookupPanelView which consists of a TextBox and a ListBox. It has an attached property ItemsSource which the ListBox binds to so the bound data can be set from outside the control.

LookupPanelView

public partial class LookupPanelView : UserControl
{
    public static readonly DependencyProperty ItemsSourceProperty =
        DependencyProperty.Register("ItemsSource", typeof(IEnumerable), typeof(LookupPanelView));

    public IEnumerable ItemsSource
    {
        get => (IEnumerable)GetValue(ItemsSourceProperty);
        set => SetValue(ItemsSourceProperty, value);
    }

    public LookupPanelView()
    {
        InitializeComponent();
    }
}

The control's ItemsSource is bound to a property in my main ViewModel which decides what data to display.

public class MainViewModel : ViewModelBase
{
    public ObservableCollection<DomainObject> LookupPanelItems { get; private set; }

    public MainViewModel()
    {
        LookupPanelItems = // Fetch the data to display in the control.
    }
}

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    UseLayoutRounding="True">
<Grid>
    <lookupPanelView:LookupPanelView Grid.Column="0" ItemsSource="{Binding LookupPanelItems}"/>
</Grid>

I would like to extend the custom control to have search functionality where you type in the TextBox and it selects a matching item from the ListBox. This logic should be contained in the control since it should be aware of how to search it's own items. I think I need to give the control it's own ViewModel to hold the logic but then how do I access the attached property ItemsSource in the ViewModel to search the items? I would like to avoid using code-behind as much as possible for maintainability and testability.

Codemunkie
  • 433
  • 3
  • 14
  • I would vote against the ViewModel for a UserControl. This functionality is specific to the UserControl and should never have any connection with ViewModel it should only expect data and filter. BTW `LookupPanelView` reeks of `DevExpress`. – XAMlMAX Jan 03 '19 at 15:29
  • @XAMlMAX Thanks for your suggestion. Would you implement it with old-school style events in the code behind then? I'm not sure what you mean "reeks of DevExpress"? – Codemunkie Jan 03 '19 at 15:45
  • The idea behind the user controls is that they should operate without knowledge about view models. So your control should filter the items it has in it's collection, and that is it's ONLY concern. When I used UC's that's what I did with code behind. Just make sure to implement it for general purpose and not dependant on a specific type of objects in it's collection. And for Dx never mind :-) – XAMlMAX Jan 03 '19 at 15:58

3 Answers3

1

After a bit of thinking I came up with this sort of starting point for you.
First you create your control a bit like this:

<UserControl x:Class="SO_App.UC.SearchableListView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:SO_App.UC"
         mc:Ignorable="d" 
         d:DesignHeight="450" d:DesignWidth="800">
<Grid x:Name="root"><!-- This allows us to keep the Data Context inheritance -->
    <Grid.Resources>
        <CollectionViewSource Source="{Binding ItemsSource}" x:Key="Items"/> <!-- This is for us to use Filtering and so on -->
    </Grid.Resources>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition/>
    </Grid.RowDefinitions>
    <TextBox x:Name="txtSearch" Text="{Binding SearchTerm}"/>
    <!-- Placeholder -->
    <TextBlock IsHitTestVisible="False" Text="{Binding SearchTextPlaceHolder,TargetNullValue=Search, FallbackValue=Search}" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="10,0,0,0" Foreground="DarkGray">
        <TextBlock.Style>
            <Style TargetType="{x:Type TextBlock}">
                <Setter Property="Visibility" Value="Collapsed"/>
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Text, ElementName=txtSearch}" Value="">
                        <Setter Property="Visibility" Value="Visible"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBlock.Style>
    </TextBlock>
    <ListView x:Name="lstItems" Grid.Row="1" ItemsSource="{Binding Source={StaticResource Items}}"/>
</Grid>


Root element keeps the Bidning to the user control in tact, while we can use the normal binding from the parent element in our main window. Then in your MainWindow.xaml you would use it like this:

<Window x:Class="SO_App.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:vm="clr-namespace:VM;assembly=VM"
    xmlns:model="clr-namespace:Model;assembly=Model"
    xmlns:converter="clr-namespace:SO_App.Converters"
    xmlns:uc="clr-namespace:SO_App.UC"
    xmlns:local="clr-namespace:SO_App"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
    <vm:MainViewModel/>
</Window.DataContext>
<Grid>
    <uc:SearchableListView SearchTextPlaceHolder="Search" ItemsSource="{Binding Users}">
        <uc:SearchableListView.Resources>
            <DataTemplate DataType="{x:Type model:User}">
                <Grid>
                    <StackPanel>
                        <TextBlock Text="{Binding ID}"/>
                        <TextBlock Text="{Binding Name}"/>
                    </StackPanel>
                </Grid>
            </DataTemplate>
        </uc:SearchableListView.Resources>
    </uc:SearchableListView>
</Grid>


For the sake of this post here is the ViewModel:

public class MainViewModel : BaseViewModel
{
    public MainViewModel()
    {
        Users = new List<User>();
        for (int i = 0; i < 6; i++)
        {
            Users.Add(new User
            {
                ID = i,
                Name = $"John the {i + 1}",
                State = i % 2 == 0 ? "CA" : "IL",
                Cases = new List<Case>() { new Case { CaseID = (i + 1) * 10, Vendor = ((i + 1) * 10) - 2 }, new Case { CaseID = (i + 1) * 10, Vendor = ((i + 1) * 10) - 2 } }
            });
        }
    }
}  

And here is the user object:

namespace Model
{
    public class User//Ideally you would have INPC implemented here
    {
        public int ID { get; set; }
        public string Name { get; set; }
        public string State { get; set; }
        public List<Case> Cases { get; set; }
    }
}  

Hope this gives you enough information to start your implementation in the right direction and with as much MvvM as possible.

XAMlMAX
  • 2,268
  • 1
  • 14
  • 23
0

CollectionViewSource with Filter will do the trick.

Here is the basic sample to use Filter on search using CollectionViewSource

Gopichandar
  • 2,742
  • 2
  • 24
  • 54
  • Is it possible to use this technique to highlight the matching item without affecting the rest of the list? I.e. not filter the list but just select the matching item – Codemunkie Jan 03 '19 at 13:02
  • The view doesn't return entries which are filtered. You might as well inject a predicate to decide which are to be highlighted. – Andy Jan 03 '19 at 13:08
0

This logic should be contained in the control since it should be aware of how to search it's own items.

Why do you need a view model then? If the "logic should be contained in the control", then implement it there.

I think I need to give the control it's own ViewModel to hold the logic but then how do I access the attached property ItemsSource in the ViewModel to search the items?

This is in contradiction to your first sentence, but if the control really needs its own view model for some reason and the view model needs access to the control, you could simply inject with a reference to the control when you create the view model, e.g.:

public LookupPanelView()
{
    InitializeComponent();
    this.DataContext = new ViewModel(this);
}

But what you probably want is to create a custom control with a default template. This is just a class that inherit from Control and has no code-behind or XAML file. Please refer to this tutorial for an example. A UserControl is more like a composite view than a custom control with its own custom logic.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • Just a note, if you set up the DataContext of the UC like this then `ItemsSource` binding will look for that property in the UC and not the DataContext where it is being used. You need to preserve the DataContext inheritance so the Bindings can be resolved correctly. – XAMlMAX Jan 03 '19 at 18:35