I have a WPF ComboBox that binds to a set of data. I do not have permissions to modify the control directly, nor can I change the data.
I'm returned 1 item in my ComboBox, but there are actually 2 rows; the blank row and my expected value. Both appear to have an index value of 0. I don't want to see this blank row, just my expected data in the ComboBox auto-selected. I have looked through everyone's related posts in here, but none of the solutions have worked for my case. I have been programming for a long time, but still fairly new to WPF. Thanks for the help.
XAML
<MyComboBox Name="myTemplate5" MyLookup="Lookup" MyFilter="att_idn=-37" MyData="Detail" MyName="comp_tmpl_idn_srt" ModCde="31" MyEmptyValue="0" ToolTip="Have a nice day" Margin="0,2.5,30,2.5" MinWidth="120" Grid.Column="1" SelectionChanged="myTemplate5_SelectionChanged" />
C#
private void myTemplate1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
MyComboBox work = sender as MyComboBox;
if (work != null && work.HasSelectionChanged(e))
{
int compTmplId = int.Parse(work.SelectedValue.ToString());
if (!_wpfIsDumb && !ChangeComponent(compTmplId))
{
_wpfIsDumb = true;
work.SelectedItem = e.RemovedItems[0];
_wpfIsDumb = false;
}
}
}
public bool HasSelectionChanged(SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count > 0 && e.AddedItems.Count > 0)
return true;
else
return false;
}
I found the solution. The selected index did not work. The problem was with the data. I was getting a NULL value passed to the box. Once I stripped out the NULL return from SQL, then it worked as expected.
You can achieved this by setting the SelectedIndex to 0.
XAML:
<ComboBox Name="myCB"
SelectedIndex="0"
MaxWidth="200" MaxHeight="25" />
Code-behind:
namespace nsComboBox
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
myCB.Items.Add("Item 1");
myCB.Items.Add("Item 2");
myCB.Items.Add("Item 3");
myCB.SelectedIndex = 0;
}
}
}
Related
I try to create a DataGrid for WPF / MVVM which allows to manually select one ore more items from ViewModel code.
As usual the DataGrid should be able to bind its ItemsSource to a List / ObservableCollection. The new part is that it should maintain another bindable list, the SelectedItemsList. Each item added to this list should immediately be selected in the DataGrid.
I found this solution on Stackoverflow: There the DataGrid is extended to hold a Property / DependencyProperty for the SelectedItemsList:
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList",
typeof(IList),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
In the View/XAML this property is bound to the ViewModel:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ucc:CustomDataGrid Grid.Row="0"
ItemsSource="{Binding DataGridItems}"
SelectionMode="Extended"
AlternatingRowBackground="Beige"
SelectionUnit="FullRow"
IsReadOnly="True"
SelectedItemsList="{Binding DataGridSelectedItems,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1"
Margin="5"
HorizontalAlignment="Center"
Content="Select some rows"
Command="{Binding CmdSelectSomeRows}"/>
</Grid>
The ViewModel also implements the command CmdSelectSomeRows which selects some rows for testing. The ViewModel of the test application looks like this:
public class CustomDataGridViewModel : ObservableObject
{
public IList DataGridSelectedItems
{
get { return dataGridSelectedItems; }
set
{
dataGridSelectedItems = value;
OnPropertyChanged(nameof(DataGridSelectedItems));
}
}
public ICommand CmdSelectSomeRows { get; }
public ObservableCollection<ExamplePersonModel> DataGridItems { get; private set; }
public CustomDataGridViewModel()
{
// Create some example items
DataGridItems = new ObservableCollection<ExamplePersonModel>();
for (int i = 0; i < 10; i++)
{
DataGridItems.Add(new ExamplePersonModel
{
Name = $"Test {i}",
Age = i * 22
});
}
CmdSelectSomeRows = new RelayCommand(() =>
{
if (DataGridSelectedItems == null)
{
DataGridSelectedItems = new ObservableCollection<ExamplePersonModel>();
}
else
{
DataGridSelectedItems.Clear();
}
DataGridSelectedItems.Add(DataGridItems[0]);
DataGridSelectedItems.Add(DataGridItems[1]);
DataGridSelectedItems.Add(DataGridItems[4]);
DataGridSelectedItems.Add(DataGridItems[6]);
}, () => true);
}
private IList dataGridSelectedItems = new ArrayList();
}
This works, but only partially: After application start when items are added to the SelectedItemsList from ViewModel, they are not displayed as selected rows in the DataGrid. To get it to work I must first select some rows with the mouse. When I then add items to the SelectedItemsList from ViewModel these are displayed selected – as I want it.
How can I achieve this without having to first select some rows with the mouse?
You should subscribe to the Loaded event in your CustomDataGrid and initialize the SelectedItems of the Grid (since you never entered the SelectionChangedEvent, there is no link between the SelectedItemsList and the SelectedItems of your DataGrid.
private bool isSelectionInitialization = false;
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
this.isSelectionInitialization = true;
foreach (var item in this.SelectedItemsList)
{
this.SelectedItems.Clear();
this.SelectedItems.Add(item);
}
this.isSelectionInitialization = false;
}
and the SelectionChanged event handler has to be modified like this:
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!this.isSelectionInitialization)
{
this.SelectedItemsList = this.SelectedItems;
}
else
{
//Initialization from the ViewModel
}
}
Note that while this will fix your problem, this won't be a true synchronization as it will only copy the items from the ViewModel at the beginning.
If you need to change the items in the ViewModel at a later time and have it reflected in the selection let me know and I will edit my answer.
Edit: Solution to have a "true" synchronization
I created a class inheriting from DataGrid like you did.
You will need to add the using
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
this.Loaded += CustomDataGrid_Loaded;
}
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
//Can't do it in the constructor as the bound values won't be initialized
//If it is expected for the bound collection to be null initially, you could subscribe to the change of the
//dependency in order to subscribe to the collectionChanged event on the first non null value
this.SelectedItemsList.CollectionChanged += SelectedItemsList_CollectionChanged;
//We call the update in case we have already some items in the VM collection
this.UpdateUIWithSelectedItemsFromVm();
if(this.SelectedItems.Count != 0)
{
//Otherwise the items won't be as visible unless you change the style (this part is not required)
this.Focus();
}
else
{
//No focus
}
}
private void SelectedItemsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.UpdateUIWithSelectedItemsFromVm();
}
private void UpdateUIWithSelectedItemsFromVm()
{
if (!this.isSelectionChangeFromUI)
{
this.isSelectionChangeFromViewModel = true;
this.SelectedItems.Clear();
if (this.SelectedItemsList == null)
{
//Nothing to do, we just cleared all the selections
}
else
{
if (this.SelectedItemsList is IList iListFromVM)
foreach (var item in iListFromVM)
{
this.SelectedItems.Add(item);
}
}
this.isSelectionChangeFromViewModel = false;
}
else
{
//Nothing to do, the change is coming from the SelectionChanged event
}
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//If your collection allow suspension of notifications, it would be a good idea to add a check here in order to use it
if(!this.isSelectionChangeFromViewModel)
{
this.isSelectionChangeFromUI = true;
if (this.SelectedItemsList is IList iListFromVM)
{
iListFromVM.Clear();
foreach (var item in SelectedItems)
{
iListFromVM.Add(item);
}
}
else
{
throw new InvalidOperationException("The bound collection must inherit from IList");
}
this.isSelectionChangeFromUI = false;
}
else
{
//Nothing to do, the change is comming from the bound collection so no need to update it
}
}
private bool isSelectionChangeFromUI = false;
private bool isSelectionChangeFromViewModel = false;
public INotifyCollectionChanged SelectedItemsList
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList),
typeof(INotifyCollectionChanged),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
You will have to initialize the DataGridSelectedItems earlier or you there will be a null exception when trying to subscribe to the collectionChanged event.
/// <summary>
/// I removed the notify property changed from your example as it probably isn't necessary unless you really intended to create a new Collection at some point instead of just clearing the items
/// (In this case you will have to adapt the code for the synchronization of CustomDataGrid so that it subscribe to the collectionChanged event of the new collection)
/// </summary>
public ObservableCollection<ExamplePersonModel> DataGridSelectedItems { get; set; } = new ObservableCollection<ExamplePersonModel>();
I didn't try all the edge cases but this should give you a good start and I added some directions as to how to improve it. Let me know if some parts of the code aren't clear and I will try to add some comments.
I am having some troubles with my datagrid ( WPF ) and since I am already a beginner I am not so very good in it.
Maybe, someone can help me out here.
I already have the row-index and column index and what I want to have now is the value from this cell.
But I do not know how to get it.
Here is my code:
var row = datagrid.Items.IndexOf(datagrid.CurrentItem);
var column = datagrid.SelectedCells[0].Column.DisplayIndex;
How is it possible now to retrieve with these two indexes now my cell vlaue.
I must solve it somehow via the indexes!
Thank you very much for your help!
Here is the sample code that gets cell value from datagrid in WPF on button click
In your MainWindow.cs
private ObservableCollection<ItemDG> _it = new ObservableCollection<ItemDG>();
public MainWindow()
{
InitializeComponent();
_it.Add(new ItemDG() { Amount = 10 });
_it.Add(new ItemDG() { Amount = 20 });
_it.Add(new ItemDG() { Amount = 30 });
dataGrid1.ItemsSource = _it;
}
private void button1_Click_1(object sender, RoutedEventArgs e)
{
TextBlock x = dataGrid1.Columns[0].GetCellContent(dataGrid1.Items[2]) as TextBlock;
if (x != null)
MessageBox.Show(x.Text);
}
}
public class ItemDG
{
public int Amount { get; set; }
}
and in your MainWindow.xaml
<DataGrid AutoGenerateColumns="False" Name="datagrid1"/>
<Button Content="Button" Name="button1" Click="button1_Click_1" />
I want to add masking to WPF date picker control. I saw that DatePickerTextBox can not be extended any further.
So, I decided a add a interactivity behavior to it. I used following code for this:
Masked date picker class:
public class MaskedDatePicker : DatePicker
{
}
and I created a attached behavior like below:
public class DatePickerTextBoxInputMaskBehavior : Behavior<DatePickerTextBox>
{
}
Now in templates I attached the behavior:
<DatePickerTextBox x:Name="PART_TextBox"
Grid.Row="0"
Grid.Column="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Focusable="{TemplateBinding Focusable}"
Foreground="{TemplateBinding Foreground}">
<i:Interaction.Behaviors>
<cleanPoc:DatePickerTextBoxInputMaskBehavior />
</i:Interaction.Behaviors>
</DatePickerTextBox>
Now every time i open the calender from datepicker application freezes because text changed in DatePickerTextBoxInputMaskBehavior got fired recursively.
Any idea how to handle it?
The base control (DatePicker) does things with the DatePickerTextBox that you cannot really control by attaching a behavior to the DatePickerTextBox...
Get a reference to the MaskedDatePicker control itself and set the Text property of the DatePickerTextBox when the SelectedDate property of the control gets set to a new value. Have a look at the following example and let me know if you need any clarification.
Good luck!
public class DatePickerTextBoxInputMaskBehavior : Behavior<DatePickerTextBox>
{
...
containing the event data.
private void AssociatedObjectLoaded(object sender, System.Windows.RoutedEventArgs e)
{
this.Provider = new MaskedTextProvider(this.InputMask, CultureInfo.CurrentCulture);
this.Provider.Set(this.AssociatedObject.Text);
this.Provider.PromptChar = this.PromptChar;
this.SetText(this.Provider.ToDisplayString());
MaskedDatePicker dp = FindVisualParent<MaskedDatePicker>(this.AssociatedObject);
var textProp = DependencyPropertyDescriptor.FromProperty(MaskedDatePicker.SelectedDateProperty, typeof(MaskedDatePicker));
if (textProp != null)
{
textProp.AddValueChanged(dp, OnHandler);
}
}
private static T FindVisualParent<T>(DependencyObject dependencyObject) where T : DependencyObject
{
var parent = VisualTreeHelper.GetParent(dependencyObject);
if (parent == null) return null;
var parentT = parent as T;
return parentT ?? FindVisualParent<T>(parent);
}
private void OnHandler(object s, EventArgs args)
{
this.UpdateText();
}
private void UpdateText()
{
if (this.Provider.ToDisplayString().Equals(this.AssociatedObject.Text))
{
return;
}
MaskedDatePicker dp = FindVisualParent<MaskedDatePicker>(this.AssociatedObject);
if (dp != null && dp.SelectedDate.HasValue)
SetText(dp.SelectedDate.Value.ToString("dd/MM/yyyy")); //format date here...
}
/// <summary>
/// Sets the text.
/// </summary>
/// <param name="text">The text.</param>
private void SetText(string text)
{
this.AssociatedObject.Text = string.IsNullOrWhiteSpace(text) ? string.Empty : string.Format(CultureInfo.CurrentCulture, text.ToString(CultureInfo.CurrentCulture.DateTimeFormat));
}
}
}
I have a DataGrid which is bound to an ObservableCollection ProductsFound
which is exposed as a property in my ViewModel.
By typing text in a TextBox, products contained in the model that have the Code property that contains the text inserted in the TextBox are added to ProductsFound.
I found out that if the DataGrid is contained in any control such as a StackPanel or a TabItem, the Window (the program) stops responding when I try to type text into the TextBox; while if the DataGrid isn't contained in any control, everything runs normally.
Here's the code for the window:
public class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// This method just fill the dataset I pass to the model's contructor in the next line.
Init();
ProductsModel model = new ProductsModel(dataSet);
searchViewModel = new ProductsSearchViewModel(model);
DataContext = searchViewModel;
}
private ProductsSearchViewModel searchViewModel;
// This handler supports the binding between the TextBox and the MatchText property of the View Model.
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
var binding = ((TextBox)sender).GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();
}
}
And here's my ViewModel:
public class ProductsSearchViewModel : Notifier, IProductsSearchViewModel
{
public ProductsSearchViewModel(IProductsModel inModel)
{
model = inModel;
productsFound = new ObservableCollection<ProductViewModel>();
}
private string matchText;
private IProductsModel model;
private ObservableCollection<ProductViewModel> productsFound;
// This is a helper method that search for the products in the model and adds them to ProductsFound.
private void Search(string text)
{
Results.Clear();
foreach (Product product in model.Products)
{
if (product.Code.ToLower().Contains(text.ToLower()))
Results.Add(new ProductViewModel(product));
}
}
public string MatchText
{
get { return matchText; }
// This setter is meant to be executed every time the Text property of the TextBox is changed.
set
{
if ((value != matchText) && (value != ""))
{
matchText = value;
// This raises INotifyPropertyChanged.PropertyChaged.
NotifyPropertyChanged("MatchText");
Search(value);
}
}
}
public ObservableCollection<ProductViewModel> ProductsFound
{
get
{
return productsFound;
}
set
{
productsFound = value;
NotifyPropertyChanged("Results");
}
}
}
Here's the XAML:
<Window x:Class="MyNameSpace.UI.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>
<StackPanel>
<TextBox Text="{Binding MatchText, Mode=TwoWay}" TextChanged="TextBox_TextChanged" />
<DataGrid x:Name="grid1" ItemsSource="{Binding Results}" >
</StackPanel>
</Grid>
With that StackPanel the program stops responding when I try to type text in the Textbox and no item is added to the DataGrid; but if i remove it everything runs ok.
What could the problem be? Am I missing something in how the WPF binding system works?
Is my view model coded wrong?
Thanks in advance.
Putting that StackPanel there prevents the DataGrid from acquiring a specific Height, thus it just expands down to infinity, and that breaks UI Virtualization.
Remove the StackPanel from there and use a non-infinite container, such as Grid or DockPanel.
I have spent considerable amount of time investigating this problem. Any help would be greatly appreciated.
I have a WPF ComboBox declared like this.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Click="Button_Click">Click Me</Button>
<ComboBox ItemsSource="{Binding ListOfValues}" SelectedItem="{Binding MySelectedItem}" Grid.Row="1">
</ComboBox>
<CheckBox IsChecked="{Binding IsValueChecked}" Grid.Row="2"></CheckBox>
</Grid>
In my code behind, i have these properties and i am implementing the INotifyPropertyChanged
public Window1()
{
InitializeComponent();
ListOfValues = new List<string>();
ListOfValues.Add("apple");
ListOfValues.Add("ball");
ListOfValues.Add("cat");
ListOfValues.Add("dog");
MySelectedItem = "cat";
IsValueChecked = true;
}
public List<string> ListOfValues
{
get
{
return _listOfValues;
}
set
{
_listOfValues = value;
OnPropertyChanged("ListOfValues");
}
}
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
OnPropertyChanged("MySelectedItem");
}
}
public bool IsValueChecked
{
get
{
return _isVlaueChanged;
}
set
{
_isVlaueChanged = value;
OnPropertyChanged("IsValueChecked");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
The button click event changes the MySelectedItem which is bound to the SelectedItem property of the combobox. But upon the button click nothing gets selected in the combobox. I dont understand why. This happens even if i set explicitly Mode=TwoWay. Please suggest. Note that my datacontext is set to self, so i have confirmed that data binding is happening properly by adding a checkbox
EDIT: Note that this happens in a sample WPF project. But my original project where i want this to work is a winforms app. I am using the elementhost to embed my wpf control. Is that making a difference?
The selected item needs to be set to an object in the list you have it bound to. settings it to a string with a matching value won't work. So try this:
foreach(string animal in ListOfValues)
{
if( animal == "dog")
this.MySelectedItem = animal;
}
I tried to reproduce your problem and I have some questions. Can you please show me your implementation of OnPropertyChanged? When I have a look at the MSDN (http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.onpropertychanged.aspx) this function requires a DependencyPropertyChangedEventArgs as the first parameter, not a string. And in addition, OnPropertyChanged is for notifying about changes in Dependency Properties, not for normal properties.
So I think you overloaded that method to support INotifyPropertyChanged, right?
I tried to implement a working example, this is the result:
public partial class TestWindow2 : Window, INotifyPropertyChanged
{
public TestWindow2()
{
InitializeComponent();
ListOfValues = new List<string> { "apple", "ball", "cat", "dog" };
MySelectedItem = "cat";
IsValueChecked = true;
this.DataContext = this;
}
...
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
RaisePropertyChanged("MySelectedItem");
}
}
...
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
private void RaisePropertyChanged(String name)
{
if( this.PropertyChanged != null ) this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Works perfectly for me. When I click the button, dog becoms the selected item in the combo box and the checkbox toggles its state.
If your items are a reference type (and you are just using string for an example), check that the Equals() method is returning what you expect. You might need to override the Equals method (eg this.ID ==other.ID or something like that) to get the correct behavior.