I have a WPF datagrid with a combobox column and two textbox columns. In my test case, when the screen is loaded, there are two rows in the collection to which the grid is bound. If I change the contents of any of the cells, it updates properly. However, if I add a new row to the grid, when I update the value in the combobox column, it is not updated in the source collection. The textbox columns work properly for newly added rows though. The columns are defined as such:
<DataGrid.Columns>
<DataGridComboBoxColumn Header="Type" Width="*" SelectedValueBinding="{Binding Mode=TwoWay, Path=Type.Id}"
ItemsSource="{Binding Source={StaticResource PhoneTypeList}, Path=PhoneTypes}"
SelectedValuePath="Id" DisplayMemberPath="Type" />
<DataGridTextColumn Binding="{Binding NotifyOnTargetUpdated=True, Path=Number, Mode=TwoWay, ValidatesOnExceptions=False}" Header="Number" Width="*"/>
<DataGridTextColumn Binding="{Binding NotifyOnSourceUpdated=True, Path=Extension, ValidatesOnExceptions=False}" Header="Extension" Width="*"/>
</DataGrid.Columns>
Here is the PhoneNumbers property in my viewmodel:
public ObservableCollection<PhoneNumber> PhoneNumbers
{
get
{
return _phoneNumbers;
}
set
{
if (value != _phoneNumbers)
{
_phoneNumbers = value;
OnPropertyChanged("PhoneNumbers");
}
}
}
Update: Here is my PhoneNumber class:
public class PhoneNumber : INotifyPropertyChanged
{
private string _number;
private string _extension;
private PhoneType _type;
public PhoneType Type { get { return _type; }
set { _type = value; OnPropertyChanged("Type"); } }
public string Number
{
set
{
_number = value;
OnPropertyChanged("Number");
}
get { return _number; }
}
public string Extension
{
set
{
_extension = value;
OnPropertyChanged("Extension");
}
get { return _extension; }
}
public override string ToString()
{
return Number + (!string.IsNullOrEmpty(Extension) ? " x " + Extension : "");
}
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this,
new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
First, check your output window for any binding error messages.
It looks to me like you Type property might be null when you add a new item. This essentially makes the Id property of Type inaccessible.
Try instantiating a new default Type object in your PhoneNumber Constructor
PhoneNumber()
{
_type = new PhoneType();
}
Or better yet, bind your combo box directly to the type instead of to the nested Type.Id (change SelectedValue Binding and remove the SelectedValuePath.
<DataGridComboBoxColumn Header="Type" Width="*" SelectedValueBinding="{Binding Mode=TwoWay, Path=Type}"
ItemsSource="{Binding Source={StaticResource PhoneTypeList}, Path=PhoneTypes}"
DisplayMemberPath="Type" />
Related
This is my service in MVVM. The data is added to the list and can see it in debugger but just before showing it on UI Presentation it did not show. i had applied two way binding and have use DataGridView. its showing data on the LoadData function but not loading. i am starter to the WPF
public EmployeeViewModel()
{
CurrentEmployee = new Employee();
objEmployeeService = new EmployeeService();
saveCommand = new RelayCommand(Save);
LoadData();
}
private void LoadData()
{
obj_Employee = new ObservableCollection<Employee>(objEmployeeService.GetAllEmployees());
}
#endregion
#region AddEmployee
private Employee currentEmployee;
private string message;
public string Message { get { return message; } set { message = value; OnPropertyChanged("Message"); } }
public Employee CurrentEmployee
{
get { return currentEmployee; }
set { currentEmployee = value; OnPropertyChanged("CurrentEmployee"); }
}
private RelayCommand saveCommand;
public RelayCommand SaveCommand
{
get { return saveCommand; }
}
public void Save()
{
bool IsSaved=false;
try
{
IsSaved = objEmployeeService.Add(CurrentEmployee);
LoadData();
}
catch (Exception ex)
{
Message = ex.Message;
}
if (IsSaved)
Message = "Employee Saved";
}
Here is my UI
<DataGrid x:Name="dgridEmployees"
AutoGenerateColumns="False"
Grid.Row="6"
Grid.Column="1"
Margin="3"
ItemsSource="{Binding Path = Employees, Mode=TwoWay}"
>
<DataGrid.Columns>
<DataGridTextColumn Header="EmployeeID"
Width="auto"
Binding=
"{Binding Path=Id}"></DataGridTextColumn>
<DataGridTextColumn Header="Employee Name"
Width="auto"
Binding=
"{Binding Path=Name}"></DataGridTextColumn>
<DataGridTextColumn Header="EmployeeAge"
Width="auto"
Binding=
"{Binding Path=Age}"></DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
It does not appear that this binding goes anywhere...what is Employees>
ItemsSource="{Binding Path = Employees, Mode=TwoWay}"
You only show obj_Employee being set. Maybe bind to that?
As to a re-load, ...sometimes the control needs to be signaled to announce a change after an initial bind. This is sometimes done by setting the initial binding target to null first, and then set to the new reference. Add this into your load:
obj_Employee = null;
obj_Employee = new ObservableCollection<Employee>(objEmployeeService.GetAllEmployees());
IN xaml.cs file(WPF Application) I have created a DataTable with 3 columns
Wanted to set the second column's width in xaml.cs only.
Also, want to set the second columns first row background color to blue(only for the cell which is in first row and 2nd column).
Have created 3 columns as :
DataTable dt= new DataTable();
dt.Columns.Add(new DataColumn("ABC");
Similarly, have added 2 more columns.
Want to set the second columns width
I am not fairly certain, if this is what you are looking for, but it is what I would do
First: let's say you have created a basic DataTable and filled it with some values, like this:
DataTable dt = new DataTable();
dt.Columns.Add("Key", typeof(int));
dt.Columns.Add("Material", typeof(string));
dt.Columns.Add("Price per Kilo", typeof(int));
dt.Rows.Add(1, "CobbleStone", 34);
dt.Rows.Add(2, "Wooden Planks", 12);
dt.Rows.Add(3, "Iron Ingots", 56);
which looks like this in the debugger:
Second: Get some VisualElement to display your Data. I'd suggest using a DataGrid for. So go your your MainWindows.xaml and add a DataGrid to your Grid with 3 DataGridTextColumns like this:
<DataGrid>
</DataGrid>
Since we want to add custiom properties to our Columns, we have to add AutoGenerateColumns="False" to our DataGrid, if we don't the DataGrid will automatically generate its columns based on its ItemsSource. Since we won't get any autogenerated Columns now, we also have to add 3 Columns resembling the 3 columns from our DataTable:
<DataGrid AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Key" />
<DataGridTextColumn Header="Material" />
<DataGridTextColumn Header="Price per Kilo" />
</DataGrid.Columns>
</DataGrid>
Third: Next we have to set the ItemsSource of our DataGrid. Unfortunately a DataGrid can't process a DataTable, so we first have to convert our DataTable into something the DataGrid can read. Let's generate a new Class for this and call it MaterialModel, which looks like this:
using System.ComponentModel;
using System.Runtime.CompilerServices;
class Model : INotifyPropertyChanged
{
private int m_Key;
public int Key
{
get
{
return m_Key;
}
set
{
m_Key = value;
OnPropertyChanged("Key");
}
}
private string m_Name;
public string Name
{
get
{
return m_Name;
}
set
{
m_Name = value;
OnPropertyChanged("Name");
}
}
private int m_Price;
public int Price
{
get
{
return m_Price;
}
set
{
m_Price = value;
OnPropertyChanged("Price");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
It has Properties and a PropertyChangedEventHandler, which will notify your VisualElement when the Property changes.
Fourth: The DataGrid doesn't accept DataTables, but it accepts Lists and ObserableCollections. Use a List, if you don't want to ever add/change your items at runtime. I'll use an ObserableCollection, which neeeds using System.Collections.ObjectModel; to work.
Create a Property of your List and add a PropertyChangedEventHandler to MainWindow.
public partial class MainWindow : Window
{
private ObservableCollection<MaterialModel> m_MaterialList;
public ObservableCollection<MaterialModel> MaterialList
{
get
{
return m_MaterialList;
}
set
{
m_MaterialList = value;
OnPropertyChanged("MaterialList");
}
}
public MainWindow()
{
// [...]
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
The next step would be to convert your DataTable into a ObservableCollection, so iterate through your DataTable and Convert each Row to one of your Models, like this:
MaterialList = new ObservableCollection<MaterialModel>();
foreach(DataRow row in dt.Rows)
{
MaterialModel model = new MaterialModel
{
Key = int.Parse(row["Key"].ToString()),
Name = row["Material"].ToString(),
Price = int.Parse(row["Price per Kilo"].ToString()),
};
MaterialList.Add(model);
}
Fivth: Your List is filled with Models, the next step would be to tell your DataGrid how to use your List. First, bind your List to the ItemsSource auf your DataGrid, then bind each DataGridTextColumn to one of the Properties in your MaterialModel, like this:
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MaterialList}">
<DataGrid.Columns>
<DataGridTextColumn Header="Key" Binding="{Binding Key}" />
<DataGridTextColumn Header="Material" Binding="{Binding Name}" />
<DataGridTextColumn Header="Price per Kilo" Binding="{Binding Price}" />
</DataGrid.Columns>
</DataGrid>
and you'll see the DataGrid works:
Sixth: The last step is to actually set the properties of your columns, which is pretty easy, your Requirements would look something like this:
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MaterialList}">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Key" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox Text="{Binding Key}" Background="LightBlue"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Material" Binding="{Binding Name}" Width="300" />
<DataGridTextColumn Header="Price per Kilo" Binding="{Binding Price}" />
</DataGrid.Columns>
</DataGrid>
I haven't found a way to create a DataGrid completely in Code behind as you wanted, but this would be cosnidered bad practice anyway. WPF is designed to use this connection between xaml und c#.
If you want to manage your column properties in c# anyways, this would be a proper way to do it:
in your MainWindow.xaml.cs:
private double m_SecondColumnWidth;
public double SecondColumnWidth
{
get
{
return m_SecondColumnWidth;
}
set
{
m_SecondColumnWidth = value;
OnPropertyChanged("SecondColumnWidth");
}
}
public MainWindow()
{
SecondColumnWidth = 300;
}
XAML:
<!-- right beneath your Grid -->
<Grid.Resources>
<local:ViewModel x:Key="viewModel" />
</Grid.Resources>
<DataGridTextColumn Header="Material" Binding="{Binding Name}" Width="{Binding Source={StaticResource viewModel}, Path=SecondColumnWidth}" />
This isn't exactly what you wanted, but I hope it helps any way.
I have a simple class as such:
public class Item
{
public int ID{get;set;}
public string Name{get;set;}
}
I have a List of this class in my Mainwindow.xaml.cs as such:
public List<Item> AllItems=GetAllItems();
I have four properties of Item class in my Mainwindow.xaml.cs as such:
public Item Item1{get;set;}
public Item Item2{get;set;}
public Item Item3{get;set;}
public Item Item4{get;set;}
This List:AllItems is databinded to four Comboboxes as under:
<ComboBox x:Name="cmbCode1" ItemsSource="{Binding AllItems}" DisplayMemberPath="ID" SelectedValuePath="ID" SelectedValue="{Binding Item1.ID,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Item1,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
<ComboBox x:Name="cmbCode2" ItemsSource="{Binding AllItems}" DisplayMemberPath="ID" SelectedValuePath="ID" SelectedValue="{Binding Item1.ID,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Item1,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
<ComboBox x:Name="cmbCode3" ItemsSource="{Binding AllItems}" DisplayMemberPath="ID" SelectedValuePath="ID" SelectedValue="{Binding Item1.ID,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Item1,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
<ComboBox x:Name="cmbCode4" ItemsSource="{Binding AllItems}" DisplayMemberPath="ID" SelectedValuePath="ID" SelectedValue="{Binding Item1.ID,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" SelectedItem="{Binding Item1,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>
I have four TextBoxes corresponding to these four Comboboxes as such:
<TextBlock x:Name="txtName1" Text="{Binding Item1.Name,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock x:Name="txtName2" Text="{Binding Item2.Name,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock x:Name="txtName3" Text="{Binding Item3.Name,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock x:Name="txtName4" Text="{Binding Item4.Name,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}"/>
What i want is that the user should never be able to select the same ID from more than one combobox.
Is there some simple way that could be done,especially using xaml only?How can i hide or show the items selected/unselected from other comboboxes so that the user can't select the same ID from more than one combobox ever?
So far i have tried to send the selected Item and the entire List to a MultivalueConverter and eliminating/adding items to the Lists there itself,but this seems too meesy.Any other better idea would be appreciated.
You could have a separate list for each of your ComboBoxes. You could then add a LostFocus event handler to each of them. You could use this to repopulate the lists for the other ComboBoxes to exclude the selection.
For example, if I've got 5 items in my list; initially I'll be able to select all 5 in any of my ComboBoxes. When I select Item1 in ComboBox1 the LostFocus event handler will update the lists behind ComboBoxes 2-4 to remove Item1. When I then select Item2 in ComboBox2 the LostFocus event handler will then update the lists behind ComboBoxes 3 and 4 to remove Item2. And so on...
An alternative approach might be to let the user select whatever they like and then run some kind of validation on the selected values to make sure that they're unique. This article goes through some of your options.
Personally, I'd go with the second approach; perhaps with a message above the textboxes indicating that the selection must be unique. You could indicate any errors and block any actions that rely on the selection while it's invalid but you're not having to update your data constantly which will probably lead to a smoother UI.
You can use the code to hide the selected item in different combobox
for (int count = 0; count <= cmb1.Items.Count -1; count++)
{
if((ComboBoxItem)(cmb1.Items[count])).SelectedValue==TextBox1.Text)
((ComboBoxItem)(cmb1.Items[count])).Visibility = System.Windows.Visibility.Collapsed;
}
This code you can write in selected event of the comboxes.
I guess you can write the same logic using triggers in XAML
You should handle this kind of logic in your view models. Here is an example for you that should give you the idea:
View Model:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
AllItems = new List<Item>() { new Item { Name = "1" }, new Item { Name = "2" }, new Item { Name = "3" }, new Item { Name = "4" } };
}
public List<Item> AllItems { get; set; }
private Item _item1;
public Item Item1
{
get { return _item1; }
set
{
_item1 = value;
NotifyPropertyChanged();
if (value != null)
value.CanSelect = false;
}
}
private Item _item2;
public Item Item2
{
get { return _item2; }
set
{
_item2 = value;
NotifyPropertyChanged();
if (value != null)
value.CanSelect = false;
}
}
private Item _item3;
public Item Item3
{
get { return _item3; }
set
{
_item3 = value; NotifyPropertyChanged();
if (value != null)
value.CanSelect = false;
}
}
private Item _item4;
public Item Item4
{
get { return _item4; }
set
{
_item4 = value;
NotifyPropertyChanged();
if (value != null)
value.CanSelect = false;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public class Item : INotifyPropertyChanged
{
public int ID { get; set; }
public string Name { get; set; }
private bool _canSelect = true;
public bool CanSelect
{
get { return _canSelect; }
set { _canSelect = value; NotifyPropertyChanged(); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML:
<Style x:Key="icstyle" TargetType="ComboBoxItem">
<Setter Property="IsEnabled" Value="{Binding CanSelect}" />
</Style>
...
<ComboBox x:Name="cmbCode1" ItemsSource="{Binding AllItems}" SelectedItem="{Binding Item1}" DisplayMemberPath="Name" ItemContainerStyle="{StaticResource icstyle}"/>
<ComboBox x:Name="cmbCode2" ItemsSource="{Binding AllItems}" SelectedItem="{Binding Item2}" DisplayMemberPath="Name" ItemContainerStyle="{StaticResource icstyle}"/>
<ComboBox x:Name="cmbCode3" ItemsSource="{Binding AllItems}" SelectedItem="{Binding Item3}" DisplayMemberPath="Name" ItemContainerStyle="{StaticResource icstyle}"/>
<ComboBox x:Name="cmbCode4" ItemsSource="{Binding AllItems}" SelectedItem="{Binding Item4}" DisplayMemberPath="Name" ItemContainerStyle="{StaticResource icstyle}"/>
I am trying to implement a two-ways binding between DataGrid and Collection.
What I want is delete a item in my Collection will automatically cause the DataGrid items been removing, Is there any possibility to make it ?
What I done so far is :
The code for item of my Collection.
[XmlRoot("configitem")]
public class ConfigItem : INotifyPropertyChanged
{
private bool bDelete = false;
[XmlAttribute("name")]
public string Name { get; set; }
[XmlAttribute("value")]
public string Value { get; set; }
public bool ToBeDelete {
get
{
return bDelete;
}
set
{
bDelete = value;
OnPropertyChanged("ToBeDelete");
}
}
[XmlAttribute("description")]
public string Description { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
The XAML code is :
<DataGrid.Columns>
<DataGridTemplateColumn>
<DataGridTemplateColumn.Header>
<CheckBox Content="All" x:Name="chkAll" Click="chkAll_Click" />
</DataGridTemplateColumn.Header>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox Name="chkSelect" IsChecked="{Binding ToBeDelete,
Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="15 2 0 0" Click="chkSelect_Click" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Width="2*" Binding="{Binding Name}" ClipboardContentBinding="{x:Null}" Header="ConfigName"/>
<DataGridTextColumn Width="2*" Binding="{Binding Value}" ClipboardContentBinding="{x:Null}" Header="ConfigValue"/>
<DataGridTextColumn Width="6*" Binding="{Binding Description}" ClipboardContentBinding="{x:Null}" Header="Description"/>
</DataGrid.Columns>
When I clicked the check box in first column.
The PropertyChanged event will be fired. This will make the value change to the collection item.And It worked.
When I click delete button. I try to delete the specified items from the current binding collection. The collection item are deleted. But Why the grid row for them weren't removed?
public void Delete()
{
List<ConfigItem> TobeRemovedList = configs.Where(x => x.ToBeDelete.Equals(true)).ToList();
TobeRemovedList.ForEach(x => configs.Remove(x));
}
Should I need to call bind again in delete button so that the DataGrid know the collection changed?
If what I did is far away from the best practice. Please kindly tell me how. Thanks.
Yes there is. You need to use ObservableCollection<T> as opposed to List<T> because latter doesn't supports change notifications. So data binding engine will not know that your list has changed and thus it can't update the DataGrid.
Also you need to keep the ObservableCollection<T> as a field, not as a local variable creating new again and again.
I need to set IsReadOnly property on a datagridCell depending on a property.
<WPFCtrlDG:ExtDataGrid Grid.Row="2"
InternalAddCommandHandling="False"
InternalDeleteCommandsHandling="False"
ItemsSource="{Binding Path=Attributes, Mode=TwoWay}"
Command="{Binding Path=AttributesCommand}">
<WPFCtrlDG:ExtDataGrid.Columns>
<WPFCtrlDG:ExtDataGridTextColumn Header="Attribute" Tag="ID_ATTRIBUTE" Width="*" IsReadOnly="{Binding Path=FL_COMMON, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"/>
<WPFCtrlDG:ExtDataGridTextColumn Header="Value" Tag="ID_VALUE" Width="*" IsReadOnly="true"/>
</WPFCtrlDG:ExtDataGrid.Columns>
</WPFCtrlDG:ExtDataGrid>
public BindingList<SPC_SPL2_ATTRIBUTE> Attributes
{
get
{
if (Context.SPC_SPL2_ATTRIBUTE == null)
Controller.Execute(delegate(IResult result)
{
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("FL_ACTIVE", true);
parameters.Add("CD_SPL2", CurrentSPL2.CD_SPL2);
Model.Invalidate(typeof(SPC_SPL2_ATTRIBUTE), Filter.GENERIC<SPC_SPL2_ATTRIBUTE>(parameters, "ID_ATTRIBUTE"));
if (Model.Appendload(result) == false)
return false;
return result.Successful;
});
return Context.SPC_SPL2_ATTRIBUTE;
}
set { Context.SPC_SPL2_ATTRIBUTE = value; }
}
FL_COMMON is a boolean property, and is inside the objects displayed in the datagrid, but the code I wrote is not working, while if I set IsreadOnly to true it works.
What am I doing wrong?
thank you
Do you call OnPropertyChanged("FL_COMMON"); in the setter?
The class that contains FL_COMMON should implement INotifyPropertyChanged and the property should look like something like this:
private bool _FL_COMMON;
public bool FL_COMMON
{
get { return _FL_COMMON; }
set
{
_FL_COMMON = value;
OnPropertyChanged("FL_COMMON");
}
}