How to disable ItemsSource synchronization with Text property of ComboBox - c#

I use the ComboBox for binding to string property of view model. I choose the ComboBox instead of TextBox, because i want to have an option to choose from the list (as a suggestion), but I don't want to change the selected text if the ItemsSource changes.
I tried to set the IsSynchronizedWithCurrentItem property to false, but when the list of suggestions change (at the position of the selected text), the Text changes to empty.
It seems that the ComboBox has remembered that the entered text was also in the list and when this item disappears, the Text property is also cleared.
So my question is: Is that a bug, or am I doing something wrong?
If it is a bug, could you suggest some work around?
I created a sample project which preproduces this:
in XAML:
<Window x:Class="TestProject1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ComboBox IsSynchronizedWithCurrentItem="False" ItemsSource="{Binding Items}"
IsEditable="True" Text="{Binding SelectedText, UpdateSourceTrigger=PropertyChanged}"
HorizontalAlignment="Left" Margin="10,39,0,0" VerticalAlignment="Top" Width="120"/>
<Button Click="Button_Click" Content="Update list"
HorizontalAlignment="Left" Margin="10,82,0,0" VerticalAlignment="Top" Width="75"/>
</Grid>
</Window>
In code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow() {
InitializeComponent();
this.DataContext = this;
Items = new List<string>() { "0", "1", "2" };
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
private List<string> _items;
public List<string> Items {// I use IEnumerable<string> with LINQ, but the effect is the same
get { return _items; }
set {
if (_items != value) {
_items = value;
RaisePropertyChanged("Items");
}
}
}
private string _selectedText;
public string SelectedText {
get { return _selectedText; }
set {
if (_selectedText != value) {
_selectedText = value;
RaisePropertyChanged("SelectedText");
}
}
}
private void Button_Click(object sender, RoutedEventArgs e) {
var changed = Items.ToList();//clone
int index = changed.IndexOf(SelectedText);
if (index >= 0) {
changed[index] += "a";//just change the currently selected value
}
Items = changed;//update with new list
}
}

This is my fix for that issue:
public class ComboBox : System.Windows.Controls.ComboBox
{
private bool ignore = false;
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
if (!ignore)
{
base.OnSelectionChanged(e);
}
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
ignore = true;
try
{
base.OnItemsChanged(e);
}
finally
{
ignore = false;
}
}
}

After your ItemsSource has changed, raise a property changed on your selected text to refresh the UI.
So in your Items collection setter, make the change:
RaisePropertyChanged("Items");
RaisePropertyChanged("SelectedText");
EDIT: in your example you aren't just changing the ItemSource, you are changing the text of the item that is the currently selected one but having a text binding on the old text. What are you expecting to see/happen? Are you wanting the selected item to stay the same, even if its text changes?

Modify Button_Click like this (commented lines are new):
private void Button_Click(object sender, RoutedEventArgs e)
{
string tempCopy = SelectedText; // Create a copy of the current value
var changed = Items.ToList();
int index = changed.IndexOf(SelectedText);
if (index >= 0)
{
changed[index] += "a";
}
Items = changed;
SelectedText = tempCopy; // Replace the selected text with the copy we made
}
All this does is makes a copy of SelectedText before Items changes, and then replaces it once the change has been made.
Copy SelectedText
Modify items source
Replace SelectedText with the copy

Related

UWP Toolkit DataGrid - How to bind CollectionViewSource to ItemsSource of DataGrid?

I'm trying to bind a grouped collection of data items to a DataGrid. The details of the presented data are not relevant, in fact all the contents are set up with dummy data for now.
I followed the sample code found in Microsoft's Sample App and "How to: Group, sort and filter data in the DataGrid Control".
After launching the app the shown DataGrid is empty and the debug output from the binding code says:
Error: Converter failed to convert value of type 'Windows.UI.Xaml.Data.ICollectionView' to type 'IBindableIterable'; BindingExpression: Path='MyContents' DataItem='MyViewModel'; target element is 'Microsoft.Toolkit.Uwp.UI.Controls.DataGrid' (Name='null'); target property is 'ItemsSource' (type 'IBindableIterable').
This is the interesting part of my XAML:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
In my view model I have this code:
public ICollectionView MyContents { get; private set; }
public override void OnNavigatedTo(NavigationEventArgs e)
{
// Irrelevant stuff left out...
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
MyContents = collectionViewSource.View;
}
Is there a conversion from ICollectionView to IBindableIterable? If so, how is it done?
I'm well aware that the examples do the binding in the code, not in the XAML. Does this really make a difference?
If this approach is wrong, how is the correct approach?
Edit:
I'm sorry, I forgot to mention that we use the "MVVM Light Toolkit" by GalaSoft. That's why the code to build the collection is in the view model, not the code behind. And it should stay there.
This has an impact on the kind of binding. To bind to a property of the view model, we use:
<mstkcontrols:DataGrid ItemsSource="{Binding MyContents}">
But to bind to a property of the code behind, is has to be:
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents}">
In the meantime, thank you very much to all reading and making suggestions. I'm currently investigating how to connect view model and code behind.
Alright, it took me a 2-digit number of hours to find the root of this problem. There seems to be a disrupted way with Binding compared to x:Bind.
"{Binding} assumes, by default, that you're binding to the DataContext of your markup page." says the documentation "Data binding in depth". And the data context of my page is the view model.
"{x:Bind} does not use the DataContext as a default source—instead, it uses the page or user control itself." says the documentation "{x:Bind} markup extension". Well, and the compile-time generated code has no problems with the different data types.
The XAML is changed to (the Mode is important, because the default is OneTime):
<mstkcontrols:DataGrid ItemsSource="{x:Bind MyContents, Mode=OneWay}" Loaded="DataGrid_Loaded">
<!-- Irrelevant stuff left out... -->
</mstkcontrols:DataGrid>
The code behind needs a property that sends notification events. For this its class needs to inherit from INotifyPropertyChanged. You could use the methods Set() and OnPropertyChanged() shown in #NicoZhu's answer, but this cut-out shows more clearly what is important:
private ICollectionView _myContents;
public ICollectionView MyContents
{
get
{
return _myContents;
}
set
{
if (_myContents != value)
{
_myContents = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyContents)));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
if ((sender as DataGrid).DataContext is MyViewModel viewModel)
{
MyContents = viewModel.ContentsView();
}
}
The view model provides the contents view (as a collection of collections) through a method that is called from the code behind. This method is almost identical to the code I used before.
internal ICollectionView ContentsView()
{
ObservableCollection<ObservableCollection<MyItemType>> groupedCollection = new ObservableCollection<ObservableCollection<MyItemType>>();
// It doesn't matter how this grouped collection is filled...
CollectionViewSource collectionViewSource = new CollectionViewSource();
collectionViewSource.IsSourceGrouped = true;
collectionViewSource.Source = groupedCollection;
return collectionViewSource.View;
}
I follow this tutorial creating a simple sample to reproduce your issue, And binding CollectionViewSource works well. Please refer the following code. This is sample project.
Xaml
<controls:DataGrid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
AlternatingRowBackground="Transparent"
AlternatingRowForeground="Gray"
AreRowDetailsFrozen="False"
AreRowGroupHeadersFrozen="True"
AutoGenerateColumns="False"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="False"
ColumnHeaderHeight="32"
FrozenColumnCount="0"
GridLinesVisibility="None"
HeadersVisibility="Column"
HorizontalScrollBarVisibility="Visible"
IsReadOnly="False"
ItemsSource="{x:Bind GroupView, Mode=TwoWay}"
Loaded="DataGrid_Loaded"
MaxColumnWidth="400"
RowDetailsVisibilityMode="Collapsed"
RowGroupHeaderPropertyNameAlternative="Range"
SelectionMode="Extended"
VerticalScrollBarVisibility="Visible"
>
<controls:DataGrid.RowGroupHeaderStyles>
<Style TargetType="controls:DataGridRowGroupHeader">
<Setter Property="Background" Value="LightGray" />
</Style>
</controls:DataGrid.RowGroupHeaderStyles>
<controls:DataGrid.Columns>
<controls:DataGridTextColumn
Binding="{Binding Name}"
Header="Rank"
Tag="Rank"
/>
<controls:DataGridComboBoxColumn
Binding="{Binding Complete}"
Header="Mountain"
Tag="Mountain"
/>
</controls:DataGrid.Columns>
</controls:DataGrid>
Code Behind
public sealed partial class MainPage : Page, INotifyPropertyChanged
{
public ObservableCollection<Item> MyClasses { get; set; } = new ObservableCollection<Item>();
private ICollectionView _groupView;
public ICollectionView GroupView
{
get
{
return _groupView;
}
set
{
Set(ref _groupView, value);
}
}
public MainPage()
{
this.InitializeComponent();
MyClasses.Add(new Item { Name = "Nico", Complete = false });
MyClasses.Add(new Item { Name = "LIU", Complete = true });
MyClasses.Add(new Item { Name = "He", Complete = true });
MyClasses.Add(new Item { Name = "Wei", Complete = false });
MyClasses.Add(new Item { Name = "Dong", Complete = true });
MyClasses.Add(new Item { Name = "Ming", Complete = false });
}
private void DataGrid_Loaded(object sender, RoutedEventArgs e)
{
var groups = from c in MyClasses
group c by c.Complete;
var cvs = new CollectionViewSource();
cvs.Source = groups;
cvs.IsSourceGrouped = true;
var datagrid = sender as DataGrid;
GroupView = cvs.View;
}
public event PropertyChangedEventHandler PropertyChanged;
private void Set<T>(ref T storage, T value, [CallerMemberName]string propertyName = null)
{
if (Equals(storage, value))
{
return;
}
storage = value;
OnPropertyChanged(propertyName);
}
private void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I don't know how transitive WPF C# is to UWP, but this is how I do my observable collection data binding in WPF
In my window's .cs:
public partial class MainWindowView : Window, INotifyPropertyChanged
{
public MainWindowView()
{
InitializeComponent();
this.data.ItemsSource = etc;
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<Stuff_NThings> etc = new ObservableCollection<Stuff_NThings>();
private void Button_Click(object sender, RoutedEventArgs e)
{
Stuff_NThings t = new Stuff_NThings();
t.stuff = 45;
t.moreStuff = 44;
t.things = 33;
t.moreThings = 89;
etc.Add(t);
}
My class:
public class Stuff_NThings : INotifyPropertyChanged
{
private int _things;
private int _moreThings;
private int _stuff;
private int _moreStuff;
public int things
{
get
{
return _things;
}
set
{
_things = value;
NotifyPropertyChanged(nameof(things));
}
}
public int moreThings
{
get
{
return _moreThings;
}
set
{
_moreThings = value;
NotifyPropertyChanged(nameof(moreThings));
}
}
public int stuff
{
get
{
return _stuff;
}
set
{
_stuff = value;
NotifyPropertyChanged(nameof(stuff));
}
}
public int moreStuff
{
get
{
return _moreStuff;
}
set
{
_moreStuff = value;
NotifyPropertyChanged(nameof(moreStuff));
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
By setting the dataGrid's item source in the mainWindow constructor, it will automatically create the headers in the dataGrid based on the class variable names. Whenever you add an instance of Stuff'NThings (via button, other, whatever, and etc) to the observable collection, the trigger is thrown and it updates the UI. Hope some of this actually applies!

Setting checkbox selected items to False in User interface - C# WPF

I have a simple checkbox items and when items are selected, it works fine.
I put a button to unselect all selected items . In the debug mode, I can see the checked state being set to unchecked (false) although it is not reflected in the UI. Here is the code:
XAML for Listbox-Checkbox:
<ListBox x:Name="Listitems" Grid.Column="0" SelectionMode="Multiple" ItemsSource="{Binding MonthlyResults}" >
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding logdate}" IsChecked="{Binding Checked ,Mode=TwoWay}" Click="CheckBox_Click"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
XAML for UncheckALL button:
<Button Grid.Row="0" Name="ClearALL" Margin="4,10,4,75" Content="Unselect All" FontFamily="Tahoma" FontSize="12" Click="Button_Click"/>
Code behind:
private void CheckBox_Click(object sender, RoutedEventArgs e)
{
var cb = sender as CheckBox;
var item = cb.DataContext;
Listitems.SelectedItem = item;
HornerPlotPluginModel model = DataContext as HornerPlotPluginModel;
var checkedItems1 = model.MonthlyResults.Where(B => B.Checked == true);
//monthlyresults is the observable collection that populates the checkbox items
model.CDFResults.Clear(); // some function
Chart1.Series.Clear();
Chart1.Axes.Clear();
model.DisplayLogs(); // some function
DrawCurves(); // some function
}
Code behind for the UncheckAll button:
private void Button_Click(object sender, RoutedEventArgs e)
{
HornerPlotPluginModel model = DataContext as HornerPlotPluginModel;
var checkedItems1 = model.MonthlyResults.Where(B => B.Checked == true);
Listitems.SelectedItems.Clear(); //SET CHECKED ITEMS TO FALSE!!!
model.CDFResults.Clear();
Chart1.Series.Clear();
}
I did look at similar post here: WPF UserControl property change not updating
but it went over my head!
Make sure that the class where the Checked property is defined implements the INotifyPropertyChanged interface and raises the PropertyChanged event in the setter of the Checked property:
public class MonthlyReport : INotifyPropertyChanged
{
private bool _checked;
public bool Checked
{
get { return _checked; }
set { _checked = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
Then you should be able to simply set the Checked property of all those objects to false to refresh the CheckBox:
HornerPlotPluginModel model = DataContext as HornerPlotPluginModel;
foreach(var item in model.MonthlyResults)
{
item.Checked = false;
}
HornerPlotPluginModel model = DataContext as HornerPlotPluginModel;
foreach(var item in model.MonthlyResults)
{
item.Checked = false;
}

ComboBox, is it possible to have ItemsSource and SelectedValue bound to different sources?

I have two different objects that are pointing at each other. The first object represents a division in a company. That object has two collection: Employees, which is all the employees working in the division and Project, which is all the special projects that are in progress within that division. So the first object looks like this:
public class Division : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
ObservableCollection<Employee> _employees;
ObservableCollection<Project> _projects;
public Division()
{
Employees = new ObservableCollection<Employee>();
Projects = new ObservableCollection<Project>();
}
public ObservableCollection<Employee> Employees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
PropertyChanged(this, new PropertyChangedEventArgs("Employees"));
}
}
}
public ObservableCollection<Project> Projects
{
get { return _projects; }
set
{
if (_projects != value)
{
_projects = value;
PropertyChanged(this, new PropertyChangedEventArgs("Projects"));
}
}
}
public void AddNewProject()
{
this.Projects.Add(new Project(this));
}
}
Notice that when adding a new project to the division, I pass a reference to the division into that project, which looks like this:
public class Project : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
string _projectName;
DateTime _deadline = DateTime.Now;
Division _division;
ObservableCollection<Employee> _members;
public Project()
{
Members = new ObservableCollection<Employee>();
}
public Project(Division div)
{
Members = new ObservableCollection<Employee>();
Division = div;
}
public string ProjectName
{
get { return _projectName; }
set
{
if (_projectName != value)
{
_projectName = value;
PropertyChanged(this, new PropertyChangedEventArgs("ProjectName"));
}
}
}
public DateTime Deadline
{
get { return _deadline; }
set
{
if (_deadline != value)
{
_deadline = value;
PropertyChanged(this, new PropertyChangedEventArgs("Deadline"));
}
}
}
public Division Division
{
get { return _division; }
set
{
if (_division != value)
{
if (_division != null)
{
_division.Employees.CollectionChanged -= members_CollectionChanged;
}
_division = value;
if (_division != null)
{
_division.Employees.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Division"));
}
}
}
public ObservableCollection<Employee> Members
{
get { return _members; }
set
{
if (_members != value)
{
if (_members != null)
{
_members.CollectionChanged -= members_CollectionChanged;
}
_members = value;
if (_members != null)
{
_members.CollectionChanged += members_CollectionChanged;
}
PropertyChanged(this, new PropertyChangedEventArgs("Members"));
}
}
}
public ObservableCollection<Employee> AvailableEmployees
{
get
{
if (Division != null){
IEnumerable<Employee> availables =
from s in Division.Employees
where !Members.Contains(s)
select s;
return new ObservableCollection<Employee>(availables);
}
return new ObservableCollection<Employee>();
}
}
void members_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
PropertyChanged(this, new PropertyChangedEventArgs("AvailableEmployees"));
}
}
The reason I'm doing it like this is, that the project could have any type of team working on it, but only from within the division. So, when building a dashboard for the division, the manager could select any of the employees to that project but without putting in an employee that is already assigned to it. So, the AvailableEmployees property in the project object always keeps track of who is not already assigned to that project.
The problem I'm having is how to translate this into a UI. The experiment I've done so far looks like this:
<UserControl x:Class="Test.Views.TestView"
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"
xmlns:local="clr-namespace:Test.Views"
d:DesignHeight="300" d:DesignWidth="300">
<StackPanel>
<ListBox ItemsSource="{Binding Div.Projects}">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="Transparent"
BorderThickness="0, 0, 0, 2"
BorderBrush="Black"
Margin="0, 0, 0, 5"
Padding="0, 0, 0, 5">
<StackPanel>
<TextBox Text="{Binding ProjectName}"/>
<ListBox ItemsSource="{Binding Members}">
<ListBox.ItemTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
Text="{Binding FirstName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add Employee to Project"
Command="{Binding RelativeSource={RelativeSource AncestorType=local:TestView}, Path=DataContext.AddEmployeeToProject}"
CommandParameter="{Binding}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<Button Content="Add New Project"
Command="{Binding AddNewProject}" />
</StackPanel>
The view model associated with this view is as follows:
public class TestViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private Division _div;
public TestViewModel(Division div)
{
Div = div;
AddNewProject = new DelegateCommand(OnAddNewProject);
AddEmployeeToProject = new DelegateCommand<Project>(OnAddEmployeeToProject);
}
public DelegateCommand AddNewProject { get; set; }
public DelegateCommand<Project> AddEmployeeToProject { get; set; }
public Division Div
{
get { return _div; }
set
{
if (_div != value)
{
_div = value;
PropertyChanged(this, new PropertyChangedEventArgs("Div"));
}
}
}
private void OnAddNewProject()
{
Div.AddNewProject();
}
private void OnAddEmployeeToProject(Project proj)
{
var availables = proj.AvailableEmployees;
if (availables.Count > 0)
{
proj.Members.Add(availables[0]);
}
}
}
However, I cannot get the combobox for each employee in each project to work. It seems like the selected item/value is bound to the itemssource, and each time the combobox turns out blank. I've tried to do this also with SelectedValue and SelectedItem properties for the combobox, but none worked.
How do I get these two separated. Is there anything else I'm missing here?
OK. After so many experiments the best solution I came up with was to create my own user control that is composed of both a button and a combobox that imitate the behavior I was expecting of the combobox on it own.
First, I had a really stupid mistake in the model where both lists of members Project and Division contain the same instances of Employee, which makes the AvailableEmployees property buggy. What I really needed to do is to create a list of copies of employees in the Project instead of just references.
In any case, I created a new user control and called it DynamicSourceComboBox. The XAML of this control looks like this:
<Grid>
<Button x:Name="selected"
Content="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=SelectedValue}"
Click="selected_Click"/>
<ComboBox x:Name="selections"
ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=ItemsSource}"
DisplayMemberPath="{Binding RelativeSource={RelativeSource AncestorType=local:DynamicSourceComboBox}, Path=DisplayMemberPath}"
Visibility="Collapsed"
SelectionChanged="selections_SelectionChanged"
MouseLeave="selections_MouseLeave"/>
</Grid>
I have here a few bindings from the button and the combobox to properties in my user control. These are actually dependency properties. The code-behind of my user control looks like this:
public partial class DynamicSourceComboBox : UserControl
{
public DynamicSourceComboBox()
{
InitializeComponent();
}
public object SelectedValue
{
get { return (object)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register("SelectedValue", typeof(object), typeof(DynamicSourceComboBox), new PropertyMetadata(null));
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
ComboBox.ItemsSourceProperty.AddOwner(typeof(DynamicSourceComboBox));
public string DisplayMemberPath
{
get { return (string)GetValue(DisplayMemberPathProperty); }
set { SetValue(DisplayMemberPathProperty, value); }
}
public static readonly DependencyProperty DisplayMemberPathProperty =
ComboBox.DisplayMemberPathProperty.AddOwner(typeof(DynamicSourceComboBox));
private void selected_Click(object sender, RoutedEventArgs e)
{
selected.Visibility = Visibility.Hidden;
selections.Visibility = Visibility.Visible;
selections.IsDropDownOpen = true;
}
private void selections_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
if (e.AddedItems.Count == 1)
{
var item = e.AddedItems[0];
Type itemType = item.GetType();
var itemTypeProps = itemType.GetProperties();
var realValue = (from prop in itemTypeProps
where prop.Name == DisplayMemberPath
select prop.GetValue(selections.SelectedValue)).First();
SelectedValue = realValue;
}
}
private void selections_MouseLeave(object sender, MouseEventArgs e)
{
selections.Visibility = Visibility.Collapsed;
selected.Visibility = Visibility.Visible;
selections.IsDropDownOpen = false;
}
}
These dependency properties imitate the properties with similar names in ComboBox but they are hooked up to the internal combobox and the button in a way that makes them behave together as a single complex combobox.
The Click event in the button hides it and present the combobox to make the effect of just a box that is opening. Then I have a SelectionChanged event in the combobox firing to update all the needed information and a MouseLeave event just in case the user doesn't make any real selection change.
When I need to use the new user control, I set it up like this:
<local:DynamicSourceComboBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorLevel=1, AncestorType=ListBox}, Path=DataContext.AvailableEmployees}"
DisplayMemberPath="FirstName"
SelectedValue="{Binding FirstName, Mode=TwoWay}"/>
Of course, for all of it to work, I have to make a lot of hookups with PropertyChanged events in the models, so the Projects instance will know to raise a PropertyChanged event for AvailableEmployees any time a change is made, but this is not really the concern of this user control itself.
This is a pretty clunky solution, with a lot of extra code that is a bit hard to follow, but it's really the best (actually only) solution I could have come up with to the problem I had.

WPF Listview item highlighting

I have a listview which contains the customer informations. There is a search text box above the that listview. When you type anything into the textbox then it higlights the matched item in the listview. But , the problem is that it makes search only in the visual side of the listview. It doesn't search in the not scrolled side of the listview(buttom of the listview). My code is below. Please have a look.
private void FindListViewItem(DependencyObject obj)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
{
ListViewItem lv = obj as ListViewItem;
if (lv != null)
{
HighlightText(lv);
}
FindListViewItem(VisualTreeHelper.GetChild(obj as DependencyObject, i));
}
}
private void HighlightText(Object itx)
{
if (itx != null)
{
if (itx is TextBlock)
{
Regex regex = new Regex("(" +TxtSearch.Text + ")", RegexOptions.IgnoreCase);
TextBlock tb = itx as TextBlock;
if (TxtSearch.Text.Length == 0)
{
string str = tb.Text;
tb.Inlines.Clear();
tb.Inlines.Add(str);
return;
}
string[] substrings = regex.Split(tb.Text);
tb.Inlines.Clear();
foreach (var item in substrings)
{
if (regex.Match(item).Success)
{
Run runx = new Run(item);
runx.Background = Brushes.Lime;
tb.Inlines.Add(runx);
if (tb.IsMouseOver)
{
tb.IsEnabled = false;
}
}
else
{
tb.Inlines.Add(item);
tb.IsEnabled = false;
}
}
return;
}
else
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(itx as DependencyObject); i++)
{
HighlightText(VisualTreeHelper.GetChild(itx as DependencyObject, i));
}
}
}
}
This happens because the ListView, by default, uses virtualization for its content. This means that the ListViewItems are created when they are needed. If you didn't scroll the ListView, some ListViewItems will not be created and VisualTreeHelper.GetChildrenCount will not be able to return those ListViewItems.
To achieve what you want, you can:
disable ListView virtualization by setting: VirtualizingStackPanel.IsVirtualizing="False" on your ListView (not recommended if you have many items in your list).
you can enforce the creation of the ListViewItem which are not visible by calling IItemContainerGenerator.GenerateNext and IItemContainerGenerator.PrepareItemContainer (not recommended at all). (also take a look at this)
find a better logic to highlight your ListViewItems :) (recommended). (for example search on your collection for the items you want to highlight instead of searching on the UI elements that are only displaying your items. Then mark the items found as highlighted and base on this, display the ListViewItems accordingly (with a different template or style))
You can do this in several ways. Here is one way that i think would work in your scenario and partially with your code and still use virtualization.
Use a data template for the list view item, and create an event handler for loaded event, something like:
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" Loaded="FrameworkElement_OnLoaded"/>
</DataTemplate>
</ListView.ItemTemplate>
In the OnLoaded event handler call your HighlightText method on the sender:
HighlightText(sender)
In order to trigger the loaded event you'll need to refresh the list view each time the search string will change. Something like ListView.Items.Refresh() should do it.
You could improve this a bit by adding a small timer on the search text changed, so the user will be able to finish typing when it's searching for something.
There are other, more elegant ways to handle this, but for your case i think this should work.
In Addition to my Comment:
Use a Property and a Observable Collection and directly filter on that Collection.
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public ObservableCollection<Entry> MyCollection {get;set;}
public MainWindow()
{
InitializeComponent();
MyCollection = new ObservableCollection<Entry>();
MyCollection.Add(new Entry() { Name = "Test" });
MyCollection.Add(new Entry() { Name = "ABCD" });
MyCollection.Add(new Entry() { Name = "TESTABC" });
MyCollection.Add(new Entry() { Name = "BCDtest" });
this.MyListView.DataContext = this;
}
private void searchTerm_KeyUp(object sender, KeyEventArgs e)
{
String term = ((TextBox)sender).Text;
foreach (Entry entry in this.MyCollection)
{
if (entry.Name.Contains(term))
entry.Highlight();
else
entry.UnHighlight();
}
}
}
public class Entry : INotifyPropertyChanged
{
public String Name { get; set; }
public Color BGColor { get; set; }
public SolidColorBrush BGBrush
{
get
{
return new SolidColorBrush(this.BGColor);
}
}
public Entry()
{
this.UnHighlight();
}
public void Highlight()
{
this.BGColor = Colors.Yellow;
this.NotifyPropertyChanged("BGBrush");
}
public void UnHighlight()
{
this.BGColor = Colors.White;
this.NotifyPropertyChanged("BGBrush");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
along with
<Grid>
<DockPanel>
<TextBox DockPanel.Dock="Top" Name="searchTerm" KeyUp="searchTerm_KeyUp"></TextBox>
<ListView Name="MyListView" ItemsSource="{Binding MyCollection}" >
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Background="{Binding BGBrush}" Text="{Binding Name}"></TextBlock>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</DockPanel>
</Grid>
And you are done. No need to manually touch the listview at any time. (To Increase Performance: For the Raising of the PropertyChanged Event you may want to add a check, if its really changing, or if it has been set to white from white etc.)

MVVM Light Binding to Observable Collection

I am using MVVM light in conjunction with EF4 and SQL CE 4, but I am having issues with my observable collection. My application doesn't neccessarily need to use the mvvm pattern, but since I need the benefits of an observablecollection I have decided to learn how to integrate it. I can successfully link my database of property entitites to my listbox and display them, I can also link some properties of these entities to textboxes, but where I am stuck is when I try to update these properties by typing in the textbox. Here is my xaml code for a textbox and the listbox:
<TextBox Text="{Binding SaleTitle, ValidatesOnDataErrors=true, Mode=TwoWay}"
<ListBox Height="424"
Margin="24,80,0,0"
x:Name="listBoxProperties"
VerticalAlignment="Top"
ItemTemplate="{StaticResource propertySummaryTemplate}"
IsSynchronizedWithCurrentItem="True"
Width="216" BorderThickness="0" Background="{x:Null}"
FontFamily="Segoe UI"
ItemsSource="{Binding PropertyList}"
SelectedItem="{Binding CurrentProperty, Mode=TwoWay}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
UseLayoutRounding="True"
HorizontalAlignment="Left"
ScrollViewer.VerticalScrollBarVisibility="Disabled" >
</ListBox>
Here is the code of part of my MainViewModel:
private string _SaleTitle;
public string SaleTitle
{
get
{
if (CurrentProperty != null)
return CurrentProperty.SaleTitle;
else
return "";
}
set
{
_SaleTitle = value;
RaisePropertyChanged("SaleTitle");
}
}
private RelayCommand loadCommand;
public ICommand LoadCommand
{
get
{
if (loadCommand == null)
loadCommand = new RelayCommand(() => Load());
return loadCommand;
}
}
private void Load()
{
PropertyList = new ObservableCollection<Property>((from property in entities.Properties.Include("Images")
select property));
propertyView = CollectionViewSource.GetDefaultView(PropertyList);
if (propertyView != null)
propertyView.CurrentChanged += new System.EventHandler(propertyView_CurrentChanged);
RaisePropertyChanged("CurrentContact");
RaisePropertyChanged("SaleTitle");
RaisePropertyChanged("Address");
RaisePropertyChanged("AuctioneerName");
RaisePropertyChanged("AgentName");
RaisePropertyChanged("Price");
RaisePropertyChanged("NextBid");
RaisePropertyChanged("Status");
}
void propertyView_CurrentChanged(object sender, System.EventArgs e)
{
RaisePropertyChanged("CurrentContact");
RaisePropertyChanged("SaleTitle");
RaisePropertyChanged("Address");
RaisePropertyChanged("AuctioneerName");
RaisePropertyChanged("AgentName");
RaisePropertyChanged("Price");
RaisePropertyChanged("NextBid");
RaisePropertyChanged("Status");
}
private Property _CurrentProperty;
public Property CurrentProperty
{
get
{
if (propertyView != null)
return propertyView.CurrentItem as Property;
return null;
}
set
{
_CurrentProperty = value;
RaisePropertyChanged("CurrentProperty");
}
}
public ObservableCollection<Property> PropertyList
{
get
{
return propertyList;
}
set
{
if (propertyList == value)
{
return;
}
var oldValue = propertyList;
propertyList = value;
// Update bindings, no broadcast
RaisePropertyChanged(PropertiesPropertyName);
}
}
public MainViewModel()
{
if (IsInDesignMode)
{
// Code runs in Blend --> create design time data.
}
else
{
// Code runs "for real"
entities = new Model1Container1();
}
}
////public override void Cleanup()
////{
//// // Clean up if needed
//// base.Cleanup();
////}
}
}
The listbox is populated successfully with the content from current selected item, but when I type in it and click out of it or do anything to lose focus it simply goes back to what was there before.
Take a look at your SaleTitle property definition. It Reads value from CurrentProperty.Saletitle but sets value to local field which is not used anythere.

Categories