Modify WPF Combobox Usercontrol to work with generic data

Viewed 69

I have created a usercontrol that is essentially an image with a combobox to provide some custom functionality. My current code has the object types hardcoded and I need help/pointers modifying the code to allow this to function on any object data types so I dont have to re-write this code for every combobox with different object types. From what I read online there is no way to make my usercontrol use a generic type . Is there anyway I can refactor this code to get the same result. Here is the result that I am looking for(and currently have). It is basically a combobox with an icon and it allows filtering the drop down based on the values entered. Result output

My simplified working code - two object types Fruit and Vegetable that share a IsFruitOrVegetable interface:

public class Fruit: IsFruitOrVegetable, INotifyPropertyChanged
{
    private string name;

    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }

    public FoodType Type { get; set; }

    public Fruit()
    {
        this.Type = FoodType.Fruit;
    }

    public Fruit(string _Name)
    {
        this.Name = _Name;
        this.Type = FoodType.Fruit;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string PropertyName)
    {
        var property = PropertyChanged;
        if (property != null)
            property(this, new PropertyChangedEventArgs(PropertyName));
    }

}


public class Vegetable : IsFruitOrVegetable, INotifyPropertyChanged
{

    private string name;

    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }

    public FoodType Type { get; set; }

    public Vegetable()
    {

        this.Type = FoodType.Vegetable;
    }

    public Vegetable(string _Name)
    {
        this.Name = _Name;
        this.Type = FoodType.Vegetable;
    }


    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string PropertyName)
    {
        var property = PropertyChanged;
        if (property != null)
            property(this, new PropertyChangedEventArgs(PropertyName));
    }
}

public enum FoodType
{
    None,
    Vegetable,
    Fruit
}

public interface IsFruitOrVegetable
{
    FoodType Type { get; set; }
    string Name { get;set;}
}

public class NoneType : IsFruitOrVegetable
{
    public FoodType Type { get; set; }
    public string Name { get; set; }
    public NoneType()
    {
        Name = "NotFound";
        Type = FoodType.None;
    }
}

Customer object that holds a IsFruitOrVegetable:

public class Customer : INotifyPropertyChanged
{

    private string name;

    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }

    private IsFruitOrVegetable item;

    public IsFruitOrVegetable Item
    {
        get
        {
            return item;
        }
        set
        {
            item = value;
            RaisePropertyChanged("Item");
        }
    }
    

    public Customer()
    {
       
    }

    public Customer(string _Name)
    {
        this.Name = _Name;
        Item = null;
        
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string PropertyName)
    {
        var property = PropertyChanged;
        if (property != null)
            property(this, new PropertyChangedEventArgs(PropertyName));
    }
}

My ViewModel:

public class MainViewModel : INotifyPropertyChanged
{
    private ObservableCollection<Customer> customersList;

    public ObservableCollection<Customer> CustomersList
    {
        get
        {
            return customersList;
        }
        set
        {
            customersList = value;
        }
    }

    // This is static, so that the ComboBox can revert back to all of the items
    private static ObservableCollection<IsFruitOrVegetable> itemsList;

    public static ObservableCollection<IsFruitOrVegetable> ItemsList
    {
        get
        {
            return itemsList;
        }
        set
        {
            itemsList = value;
        }
    }

    public MainViewModel()
    {
        ItemsList = new ObservableCollection<IsFruitOrVegetable>();
        ItemsList.Add(new Fruit("Apple"));
        ItemsList.Add(new Fruit("Banana"));
        ItemsList.Add(new Fruit("Avocado"));
        ItemsList.Add(new Fruit("Blueberries"));
        ItemsList.Add(new Fruit("Watermelon"));

        ItemsList.Add(new Vegetable("Broccoli"));
        ItemsList.Add(new Vegetable("Cabbage"));
        ItemsList.Add(new Vegetable("Carrot"));
        ItemsList.Add(new Vegetable("Cauliflower"));
        ItemsList.Add(new Vegetable("Potato"));
        ItemsList.Add(new Vegetable("Cucumber"));
        ItemsList.Add(new Vegetable("Kale"));
        ItemsList.Add(new Vegetable("Bellpepper"));

        CustomersList = new ObservableCollection<Customer>();
        CustomersList.Add(new Customer("Bob"));
        CustomersList.Add(new Customer("Tony"));
        CustomersList.Add(new Customer("John"));

        CustomersList[0].Item = ItemsList[0];
        CustomersList[1].Item = ItemsList[1];
        CustomersList[2].Item = ItemsList[2];
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string PropertyName)
    {
        var property = PropertyChanged;
        if (property != null)
            property(this, new PropertyChangedEventArgs(PropertyName));
    }
}

Usercontrol.xaml:

        <Grid DataContext="{Binding ElementName=Root}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="30"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <!-- Image at the side of the ComboBox -->
        <Image Height="30" Width="30" Grid.Column="0">
            <Image.Style>
                <Style TargetType="{x:Type Image}">
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding SelectedItem.Type}" Value="{x:Static local:FoodType.Vegetable}">
                            <Setter Property="Source" Value="/Resources/VegetableIcon.png"/>
                        </DataTrigger>
                        <DataTrigger Binding="{Binding SelectedItem.Type}" Value="{x:Static local:FoodType.Fruit}">
                            <Setter Property="Source" Value="/Resources/FruitIcon.png"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </Image.Style>
        </Image>

        <!-- Actual ComboBox -->
        <ComboBox x:Name="CustomCombo" Grid.Column="1" 
                  IsTextSearchEnabled="False" IsReadOnly="True" KeyDown="CustomCombo_KeyDown"
                  DropDownOpened="CustomCombo_DropDownOpened" DropDownClosed="CustomCombo_DropDownClosed"
                  IsEditable="True" TextSearch.TextPath="Name"
                  Text="{Binding Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
                  SelectedItem="{Binding SelectedItem, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                  ItemsSource="{Binding ItemsSource, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <!-- StackPanel consisting of an Image and TextBlock as ItemTemplate-->
                        <Image Height="30" Width="30">
                            <Image.Style>
                                <Style TargetType="{x:Type Image}">
                                    <Style.Triggers>
                                        <DataTrigger Binding="{Binding Path=Type}" Value="{x:Static local:FoodType.Vegetable}">
                                            <Setter Property="Source" Value="/Resources/VegetableIcon.png"/>
                                        </DataTrigger>
                                        <DataTrigger Binding="{Binding Path=Type}" Value="{x:Static local:FoodType.Fruit}">
                                            <Setter Property="Source" Value="/Resources/FruitIcon.png"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Image.Style>
                        </Image>
                        <TextBlock Text="{Binding Path=Name}" VerticalAlignment="Center"/>
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </Grid>

CustomComboBox.xaml.cs:

public partial class CustomComboBox : UserControl, INotifyPropertyChanged
{
    #region States
    public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(
            "ItemsSource", 
            typeof(ObservableCollection<IsFruitOrVegetable>),
            typeof(CustomComboBox),
            new PropertyMetadata(null)
        );
    public ObservableCollection<IsFruitOrVegetable> ItemsSource
    {
        get
        {
            return (ObservableCollection<IsFruitOrVegetable>)GetValue(ItemsSourceProperty);
        }
        set
        {
            SetValue(ItemsSourceProperty, value);
            RaisePropertyChanged("ItemsSource");
        }
    }

    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register(
            "SelectedItem",
            typeof(IsFruitOrVegetable),
            typeof(CustomComboBox),
            new PropertyMetadata(null)
        );
    public IsFruitOrVegetable SelectedItem
    {
        get
        {
            return (IsFruitOrVegetable)GetValue(SelectedItemProperty);
        }
        set
        {
            SetValue(SelectedItemProperty, value);
            RaisePropertyChanged("SelectedItem");
        }
    }

    private string text;
    public string Text
    {
        get
        {
            return text;
        }
        set
        {
            text = value;

            // Search Function
            if (value != null && value != "")
            {
                var items = new ObservableCollection<IsFruitOrVegetable>();
                foreach (var item in MainViewModel.ItemsList)
                {
                    if (item.Name.ToLower().Contains(Text.ToLower()))
                    {
                        items.Add(item);
                    }
                }
                if (items.Count == 0)
                {
                    items.Add(new NoneType());
                }
                ItemsSource = items;
            } else
            {
                ItemsSource = MainViewModel.ItemsList;
            }

            RaisePropertyChanged("Text");
            RaisePropertyChanged("ItemsSource");
        }
    }
    #endregion
    public CustomComboBox()
    {
        InitializeComponent();

        // Sets initial Last Selected Item
        if (SelectedItem != null)
        {
            lastSelectedItem = SelectedItem;
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string PropertyName)
    {
        var property = PropertyChanged;
        if (property != null)
            property(this, new PropertyChangedEventArgs(PropertyName));
    }

    #region Event Handlers
    private IsFruitOrVegetable lastSelectedItem;

    // Saves Last Selected Item when the drop down is opened
    private void CustomCombo_DropDownOpened(object sender, EventArgs e)
    {
        ComboBox comboBox = sender as ComboBox;
        comboBox.IsReadOnly = false;


        if (SelectedItem != null && SelectedItem.Type != FoodType.None)
        {
            lastSelectedItem = SelectedItem;
        }
        Text = "";
    }

    // Validates if Selected Item is of Type None, then it will revert back to 
    // the last selected Item which is saved
    private void CustomCombo_DropDownClosed(object sender, EventArgs e)
    {
        ComboBox comboBox = sender as ComboBox;
        comboBox.IsReadOnly = true;

        if (SelectedItem == null || SelectedItem.Type == FoodType.None)
        {
            if (lastSelectedItem != null)
            {
                SelectedItem = lastSelectedItem;
                Text = SelectedItem.Name;
            } else
            {
                SelectedItem = null;
                Text = "";
            }
        } else if (SelectedItem != null && SelectedItem.Type != FoodType.None)
        {
            Text = SelectedItem.Name;
        }
    }
    #endregion

    private void CustomCombo_KeyDown(object sender, KeyEventArgs e)
    {
        ...
    }
}

Finally my view:

    <Window.Resources>
        <vm:MainViewModel x:Key="viewModel"/>
    </Window.Resources>
    <Grid>

        <DataGrid HorizontalAlignment="Left" Height="291" Margin="22,90,0,0" VerticalAlignment="Top" Width="300"
                  ItemsSource="{Binding CustomersList,Source={StaticResource viewModel}}" AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Customer" Binding="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Width="*"/>

                <DataGridTemplateColumn Header="Item" Width="*">
                    <DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <local:CustomComboBox SelectedItem="{Binding Item, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                                ItemsSource="{Binding ItemsList, Source={StaticResource viewModel}}"/>
                        </DataTemplate>
                    </DataGridTemplateColumn.CellTemplate>
                </DataGridTemplateColumn>
            </DataGrid.Columns>
        </DataGrid>


    </Grid>
0 Answers
Related