WPF ComboBox appearance query - c#

In an example WPF Project (see code below) I have a ComboBox control that uses a DataTemplate to show its bound items (an array of MyItem objects). All this works as expected. Once an item is selected on the ComboBox, it shows the full DataTemplate for the selected item. What I would like to happen instead is
When the user opens the ComboBox, it shows the full template for each bound item.
Once an item is selected (and the ComboBox closes up) for it to only show a single property of the bound item, say the Name property of the MyItem object.
Here is some example code:
XAML:
<Window x:Class="WpfApplication4.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:local="clr-namespace:WpfApplication4"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding .}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Description}"/>
<TextBlock Text="{Binding Message}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Window>
Code behind:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MyItem[]
{
new MyItem() { Name = "Name1", Message="Message 1", Description = "Description 1" },
new MyItem() { Name = "Name2", Message="Message 2", Description = "Description 2" },
new MyItem() { Name = "Name3", Message="Message 3", Description = "Description 3" }
};
}
}
public class MyItem
{
public string Name { get; set; }
public string Description { get; set; }
public string Message { get; set; }
}

Related

RaisePropertyChanged Triggering "set" On Different Property

I was working on a project when I came across an issue with RaisePropertyChanged from MVVM Light that I can't seem to figure out. When I try to raise a change for my list, the list does get updated, but as does the selected index value above. The value that is passed to my selected index appears to be influenced by what key was pressed to trigger the event (i.e. if I press "BACKSPACE", the value passed to the setter is "-1", whereas if I enter a letter, the value passed is "0")
I recreated a project which purely demonstrates the issue. Below is the main bit of logic found in the MainVeiwModel:
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
_testItems = new List<TestItem>()
{
new TestItem() { Name = "Test1" },
new TestItem() { Name = "Test2" }
};
}
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
_selectedIndex = value;
RaisePropertyChanged("SelectedIndex");
RaisePropertyChanged("SelectedText");
RaisePropertyChanged("TestList");
}
}
public string SelectedText
{
get
{
return _testItems[_selectedIndex].Name;
}
set
{
_testItems[_selectedIndex].Name = value;
RaisePropertyChanged("TextList");
}
}
public List<string> TextList
{
get
{
_textList = new List<string>();
if (_testItems != null && _testItems.Count > 0)
{
foreach (TestItem item in _testItems)
_textList.Add(item.Name);
}
return _textList;
}
set { _textList = value; }
}
private int _selectedIndex;
private List<string> _textList;
private List<TestItem> _testItems;
}
My XAML:
<Window x:Class="RaisePropertyBug.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:local="clr-namespace:RaisePropertyBug"
mc:Ignorable="d"
DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding TextList, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding SelectedIndex, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Text="{Binding SelectedText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
For context: I have a ComboBox which lists the names from a collection of items I have. There is an edit window where users can change names and other properties of these items. My goal is to have the ComboBox list update as the user edits the value. In my actual program, you are able to do this with the item at index 0, but any other index will automatically change to 0 as soon as a key is pressed and the RaisePropertyChanged() area is reached.
Check below code if it's working as per your requirement.
Use SelectedItem property of ComboBox and bind selecteditem to the edit screen/textbox. I've bind SelectedTestItem.Name propety here.
View -
<Window x:Class="StackOverflow.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:local="clr-namespace:StackOverflow"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox ItemsSource="{Binding TestItems, UpdateSourceTrigger=PropertyChanged}" DisplayMemberPath="Name" SelectedValuePath="Name"
SelectedItem="{Binding SelectedTestItem, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Text="{Binding SelectedTestItem.Name, UpdateSourceTrigger=PropertyChanged}" Width="200"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
View.cs -
public partial class MainWindow : Window, INotifyPropertyChanged
{
private TestItem selectedTestItem;
public TestItem SelectedTestItem
{
get { return selectedTestItem; }
set
{
selectedTestItem = value;
RaisePropertyChanged("SelectedTestItem");
}
}
public List<TestItem> TestItems
{
get;
set;
}
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
var items = new List<TestItem>()
{
new TestItem() { Name = "Test1" },
new TestItem() { Name = "Test2" }
};
TestItems = items;
}
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string prop)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
}
You don't even need INotifyPropertyChanged for this example. I'm not entirely certain of what you are trying to achieve, but this code will achieve what I have gleaned from your post.
<Window x:Class="RaisePropertyChangedExample.BindingExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Correct View" Width="150" Height="80">
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding Items}"
x:Name="ItemViews"
HorizontalAlignment="Stretch" VerticalAlignment="Center" DisplayMemberPath="Name"/>
<TextBox DataContext="{Binding SelectedItem, ElementName=ItemViews}" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
</StackPanel>
</Window>
and the supporting code
using System.Windows;
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace RaisePropertyChangedExample
{
public partial class BindingExample : Window
{
public BindingExample()
{
InitializeComponent();
DataContext = new BindingExampleViewModel();
}
}
public class BindingExampleViewModel
{
public ObservableCollection<TestItemViewModel> Items { get; set; }
= new ObservableCollection<TestItemViewModel>(new List<TestItemViewModel>
{
new TestItemViewModel {Name = "Test1"},
new TestItemViewModel {Name = "Test2"}
});
}
public class TestItemViewModel
{
public string Name { get; set; }
}
}
Unless there is some need of the index of the selected Item, there is no real argument against simply exposing each item as a TestItemViewModel view model and binding the other controls directly to the selected item itself. If however other controls are bound to the members of the TestItemViewModel then it's still not necessarily true that you should implement INotifyPropertyChanged on that view model.
The following example will still display the correct information when wired up with the existing ViewModel:
<Window x:Class="RaisePropertyChangedExample.BindingExample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Correct View" Width="150" Height="100">
<StackPanel Orientation="Vertical">
<ComboBox ItemsSource="{Binding Items}"
x:Name="Combo"
HorizontalAlignment="Stretch" VerticalAlignment="Center" DisplayMemberPath="Name"/>
<Grid DataContext="{Binding SelectedItem, ElementName=Combo}">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Stretch" VerticalAlignment="Center"/>
<Label Grid.Row="1" HorizontalAlignment="Stretch" Height="20" Content="{Binding Name}" />
</Grid>
</StackPanel>
</Window>
Normally
Updating after every keystroke can diminish performance and it denies the user the usual opportunity to backspace and fix typing errors before committing to the new value. see MS reference
however, this is only an issue if other processing occurs as the result of updating the source. If you are worried about the amount of processing involved you can switch to the default behaviour of LostFocus by simply omitting `UpdateSourceTrigger' declaration.

Obtaining styles from Selecteditem in ListView using DataTemplates

I am trying to harvest data from a ListView but am having problems harvesting the data when it is coming from a DataTemplate.
//wpf code (basic)
<ListView x:Name="anotherList">
<ListViewItem Tag="aTag" Content="A"/>
<ListViewItem Tag="bTag" Content="B"/>
</ListView>
//c# code
// In order to access data in the ListView I would do this
ListViewItem selectedItem = (ListViewItem)anotherList.SelectedItem;
String selectedContent = selectedItem.Content;
Now when I try to include a DataTemplate in there, I cannot use the same method to access the data in wpf.
//wpf code using data template
<ListView x:Name="mainList">
<ListView.ItemsSource>
// Accessing data from an inline xml table
<Binding Source="{StaticResource Book1}" XPath="Entry"/>
</ListView.ItemsSource>
<ListView.ItemTemplate>
<DataTemplate>
<ListViewItem Tag="{Binding XPath=Tag}" Content="{Binding XPath=LastName}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
//c# code
//gives null
var selectedItem = (ListViewItem)mainList.SelectedItem //....;
// throws exception
String selectedContent = selectedItem.Content;
Not sure how to proceed with the c# code since my selected item comes up as a whole string of XML content with no way to access content or tag. (i.e. it is showing up in the visual tree correctly but the ListViewItem is not behaving like a ListViewItem Object.) Is there a way to change my WPF code so that I can access the styles from the back like in the basic c# code?
Really appreciate the help and thanks for reading.
This aproach based on MVVM works for me
XAML:
<Window x:Class="WpfApp1.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:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800"
d:DataContext="{d:DesignInstance local:MyViewModel}">
<Grid>
<ListView ItemsSource="{Binding Users}" SelectedItem="{Binding SelectedUser}">
<ListView.ItemTemplate>
<DataTemplate >
<Grid d:DataContext="{d:DesignInstance local:User}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="10"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding FirstName}"/>
<TextBlock Text="{Binding LastName}" Grid.Column="2"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
ViewModel:
public class MyViewModel
{
private User _selectedUser;
public MyViewModel()
{
Users = new ObservableCollection<User>
{
new User{FirstName = "User A FirstName", LastName = "User A LastName"},
new User{FirstName = "User B FirstName", LastName = "User B LastName"},
new User{FirstName = "User C FirstName", LastName = "User C LastName"}
};
}
public ObservableCollection<User> Users { get; set; }
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value;
Debug.WriteLine(_selectedUser.LastName);
}
}
}
public class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
DataContext binding:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MyViewModel();
}
}

How to get ItemTemplateSelector to work in WPF

In a WPF project I have a ComboBox to select from different objects. Using an ItemsControl and it's ItemTemplateSelector I am trying to show different UI for the ComboBox selection based on a property of the object. So, in the example below we pick from person objects. In the ItemTemplateSelector we pick a different DataTemplate based on the Person's IsManager property. The trouble is it doesn't work.
I have a suspicion it might be due to the ItemsSource of the ItemsControl being bound to one item, but I am not sure. If this is the problem, what changes can I make to the code to achieve this?
XAML :
<Window x:Class="ItemsSelector.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:local="clr-namespace:ItemsSelector"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.Resources>
<local:Selector x:Key="selector"/>
<DataTemplate x:Key="managerTemplate">
<TextBlock Text="Manager"/>
</DataTemplate>
<DataTemplate x:Key="juniorTemplate">
<TextBlock Text="Junior"/>
</DataTemplate>
</Grid.Resources>
<ComboBox x:Name="cbo" Margin="2" Grid.Row="0" ItemsSource="{Binding .}" SelectedIndex="0">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ItemsControl Grid.Row="1" ItemTemplateSelector="{StaticResource selector}" ItemsSource="{Binding ElementName=cbo ,Path=SelectedItem}">
</ItemsControl>
</Grid>
CODE BEHIND:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new Person[] {
new Person() { Name = "Boss", IsManager = true },
new Person() { Name = "Underling", IsManager = false }
};
}
}
PERSON:
public class Person
{
public string Name { get; set; }
public bool IsManager { get; set; }
public string Title { get; set; }
}
SELECTOR:
public class Selector : DataTemplateSelector
{
public override DataTemplate
SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null && item is Person)
{
var person = item as Person;
switch (person.IsManager)
{
case true:
return element.FindResource("managerTemplate") as DataTemplate;
case false:
return element.FindResource("juniorTemplate") as DataTemplate;
default:
break;
}
}
return null;
}
}
The ItemsSource property of an ItemsControl can only be bound to a collection that returns an IEnumerable.
You should use a ContentControl to be able to bind display the selected item of the ComboBox:
<ContentControl Grid.Row="1" ContentTemplateSelector="{StaticResource selector}"
Content="{Binding ElementName=cbo ,Path=SelectedItem}">
</ContentControl>
I think I've found the solution. I need to be using ContentTemplateSelector.

Binding an array to an listbox so that it shows up outside of runtime

Is there anyway to bind an array created in the C# portion of the code to a ListBox so that it shows up at design time?
Something like
XAML
<ListBox ItemsSource="{Binding MyStrings}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBox Text={Binding} />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
C#
public string[] MyStrings = new string[] {"A", "B", "C"};
Runtime DataContext will also work in design mode. All you need to do is extract out code in separate ViewModel (that's what MVVM pattern recommends as well) with array declared over there and simply bind DataContext to ViewModel.
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<StackPanel>
<ListBox ItemsSource="{Binding MyStrings}"/>
</StackPanel>
</Window>
ViewModel:
public class MainWindowViewModel
{
string[] myStrings = new string[] { "A", "B", "C" };
public string[] MyStrings
{
get
{
return myStrings;
}
}
}
Designer:
you need to create a custom type first to store data as its properties, something like this:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
then create a list using the type, like this:
List<Student> list1 = new List<Student>()
{
new Student() { Name = "Bob", Age = 12 },
new Student() { Name = "John", Age = 30 },
};
in XMAL, do this:
<Grid>
<ListBox x:Name="myList" ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="2">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
lastly at run time, initial myList DataContext like this:
myList.DataContext = list1;

How to display string in ListBox using MVVM?

I have a ListBox on a WPF form. I would like to display a list of string items horizontally. I have a Grid that holds my ListBox control.
When I run the form it displays the name of the encapsulating object: ProjectName.Folder.Category instead of the string object within it.
ViewModelLocator
public CategoryViewModel CategoryViewModel
{
get
{
if (categoryviewModel == null)
{
categoryviewModel = new CategoryViewModel();
categoryviewModel.ListData.Clear();
categoryviewModel.ListData.Add(new Category { MyCategory = "new categroy1" });
categoryviewModel.ListData.Add(new Category { MyCategory = "new categroy2" });
categoryviewModel.ListData.Add(new Category { MyCategory = "new categroy3" });
categoryviewModel.ListData.Add(new Category { MyCategory = "new categroy4" });
categoryviewModel.ListData.Add(new Category { MyCategory = "new categroy5" });
categoryviewModel.ListData.Add(new Category { MyCategory = "new categroy6" });
}
return categoryviewModel;
}
}
Model
class Category
{
public String MyCategory { get; set; }
}
MainPage.xaml
<Grid>
<my:featureControl HorizontalAlignment="Left"
x:Name="featureControl1"
VerticalAlignment="Top" Height="332"
Loaded="featureControl1_Loaded" />
</Grid>
Control.xaml
<UserControl x:Class="AmebaPrototype.UI.Longlist.CategoryControl"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="1280">
<Grid DataContext="{Binding Source={StaticResource viewModelLocator},Path=CategoryViewModel}">
<ListBox Height="300" HorizontalAlignment="Left" Name="listBox1" VerticalAlignment="Top" Width="1280" ItemsSource="{Binding ListData}" >
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
You need to set the DisplayMemberPath in your ListBox
This seemed to have done the trick.
<DataTemplate x:Key="ListBoxTemplate">
<TextBlock x:Name="black" Text="{Binding MyCategory}"/>
</DataTemplate>

Categories

Resources