Problem with DataGrid Rows in WPF - c#

I have a datagrid where I have two buttons on each row - one for moving a row up and one for moving a row down.
Each button has a command for allowing the user to move the selected row in either direction. The problem that I am facing is that it not working. I think the problem that I may have is that the other controls (combo boxes) on the rows are bound to data sources through the MVVM model where I am manipulating the rows on the code behind of the XAML thinking this would be the logical place in which to do it.
The code I have for one of the buttons is below:
private void MoveRowDown(object sender, ExecutedRoutedEventArgs e)
{
int currentRowIndex = dg1.ItemContainerGenerator.IndexFromContainer(dg1.ItemContainerGenerator.ContainerFromItem(dg1.SelectedItem));
if (currentRowIndex >= 0)
{
this.GetRow(currentRowIndex + 1).IsSelected = true;
}
}
private DataGridRow GetRow(int index)
{
DataGridRow row = (DataGridRow)dg1.ItemContainerGenerator.ContainerFromIndex(index);
if (row == null)
{
dg1.UpdateLayout();
dg1.ScrollIntoView(selectedAttributes.Items[index]);
row = (DataGridRow)dg1.ItemContainerGenerator.ContainerFromIndex(index);
}
return row;
}

You have to manipulate the CollectionView for the DataGrid. The CollectionView is responsible for how your data looks like basically. Here is a small example:
Let's assume you've bound your DataGrid to an ObservableCollection<T> named Items, and that T has a property called Index on which is sorted.
Initialize the ICollectionView in your ViewModel like this:
private ICollectionView cvs;
ctor(){
/*your default init stuff*/
/*....*/
cvs = CollectionViewSource.GetDefaultView(items);
view.SortDescriptions.Add(new SortDescription("Index",ListSortDirection.Ascending));
}
Now you can bind your button command to the Up command (also in your ViewModel):
private ICommand upCommand;
public ICommand Up
{
get { return upCommand ?? (upCommand = new RelayCommand(p => MoveUp())); }
}
private void MoveUp()
{
var current = (Item)view.CurrentItem;
var i = current.Index;
var prev = Items.Single(t => t.Index == (i - 1));
current.Index = i - 1;
prev.Index = i;
view.Refresh(); //you need to refresh the CollectionView to show the changes.
}
WARNING: you have to add checks to see if there is a previous item etc. Alternatively you can specify a CanExecute delegate which checks the Index of the item and enables/disables the button.
(RelayCommand courtesy of Josh Smith/Karl Shifflett, can't find the correct link anymore)
Bind the command to you button like this (assuming your viewmodel is the DataContext of your window):
<DataGridTemplateColumn >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="Up"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}},
Path=DataContext.Up}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Hope this helps!

Related

Sorting an ObservableCollection after editing a property of an item

I'm trying to bind an ObservableCollection<T> to a DataGrid in WPF.
Below the DataGrid, there are fields to edit the currently selected item from the DataGridlike so:
So the generic T of the ObservableCollection<T> has the following properties:
- Title (Überschrift)
- Description (Beschreibung)
- Path (Pfad)
and it also has a property Reihenfolge which means Order.
With the yellow arrows, I want to be able to modify the order of the entries.
Unfortunately, the ObservableCollection doesn't have an OrderBy-method...
I've tried the following:
In XAML I have defined a CollectionViewSource like this:
<CollectionViewSource Source="{Binding Bilder}" x:Key="Pictures">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Reihenfolge" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
And I have binded the DataGrid to this CollectionViewSource
<DataGrid Grid.Column="0" Grid.Row="1"
Name="PictureDataGrid"
ItemsSource="{Binding Source={StaticResource Pictures}}"
AutoGenerateColumns="False"
IsReadOnly="True"
CanUserAddRows="false"
SelectedItem="{Binding SelectedBild}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
...
In the ViewModel, I have these properties:
public ObservableCollection<BildNotifiableModel> Bilder { get; set; }
public BildNotifiableModel SelectedBild { get; set; }
and two methods which are called with DelegateCommands that update the order
private void MoveSeiteUp()
{
const int smallestReihenfolge = 1;
if (this.SelectedBild.Reihenfolge > smallestReihenfolge) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge - 1);
this.SelectedBild.Reihenfolge--;
bildToSwapReihenfolgeWith.Reihenfolge++;
RaisePropertyChanged(nameof(this.Bilder));
}
}
private void MoveSeiteDown()
{
if (this.SelectedBild.Reihenfolge < MaxAllowedImages) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge + 1);
this.SelectedBild.Reihenfolge++;
bildToSwapReihenfolgeWith.Reihenfolge--;
RaisePropertyChanged(nameof(this.Bilder));
}
}
The order gets updated correctly, but unfortunately, the view doesn't reflect the changes... only after closing and reopening the view, the entries in the DataGrid are in the correct order.
What am I doing wrong here?
How can I make the DataGrid update, when changing the order?
Thanks in advance
I think the problem is that the CollectionView doesn't listen for the PropertyChanged-Events from its elements and also RaisePropertyChanged(nameof(this.Bilder)); dosen't work because the CollectionView is not really changed.
I would recomend to create the CollectionView in code via CollectionViewSource.GetDefaultView(list). So you can control the CollectionView from your model and call ICollectionView.Refresh if needed.
In your Methods, create a new Collection and add it to "Bilder". Just raising the PropertyChanged will execute an evaluation for referential equality. If it is the same - which it will be, if you just move items inside around - it will not update the DataGrid.
If you are not using the ObservableCollections attributes, like automatically updates, when items are added or removed, you might also change it to a "normal" List.
private void MoveSeiteUp()
{
const int smallestReihenfolge = 1;
if (this.SelectedBild.Reihenfolge > smallestReihenfolge) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge - 1);
this.SelectedBild.Reihenfolge--;
bildToSwapReihenfolgeWith.Reihenfolge++;
this.Bilder = new ObservableCollection<BildNotifiableModel> (this.Bilder);
RaisePropertyChanged(nameof(this.Bilder));
}
}
private void MoveSeiteDown()
{
if (this.SelectedBild.Reihenfolge < MaxAllowedImages) {
var bildToSwapReihenfolgeWith = this.Bilder.Single(b => b.Reihenfolge == this.SelectedBild.Reihenfolge + 1);
this.SelectedBild.Reihenfolge++;
bildToSwapReihenfolgeWith.Reihenfolge--;
this.Bilder = new ObservableCollection<BildNotifiableModel> (this.Bilder);
RaisePropertyChanged(nameof(this.Bilder));
}
}

Why does collection1 has the same values like collection2?

What I want
So I want to check 2 ObservableCollections if they equals each other.
If so, then return Nothing Changed (collection1 and collection2 are the same).
Otherwise return Something Changed.
The Problem
The problem now is, that both collection contains the same values even when I change items from collection 2.
I posted some Code and gif of the Debug result to show you what I get.
I dont understand, why both Collections are the same after clicking the Save Button.
Code
ViewModel
In my ViewModel I have:
1 ObservableCollection called RightsCollection.
This should contain the rights on my XAML which I can change via ToggleButton.
1 Employee class where a ObservableCollection<Groups> is located and inside of the Groups.Col there is a ObservableCollection<Rights> which contains the default group rights which was loaded from DataBase which cant be changed.
Note: My get set is always the same. They just have other names and DataTypes consider to its field datatype.
private Employee _singleEmployee = new Employee();
public Employee SingleEmployee
{
get => _singleEmployee;
set
{
if (_singleEmployee == value) return;
_singleEmployee = value;
OnPropertyChanged("SingleEmployee");
}
}
private ObservableCollection<Groups> _groupsCollection = new ObservableCollection<Groups>();
// public get set GroupsCollection (same like first).
private ObservableCollection<Rights> _rightsCollection = new ObservableCollection<Rights>();
// public get set RightsCollection (same like first).
Employee Class
public class Employee : INotifyPropertyChanged
{
private int _employeeId;
private string _firstName;
private Groups _group = new Group();
// public get set EmployeeId (Same like first).
// public get set Group (same like first).
}
Rights Class
private int _rightId;
private string _rightName;
private bool _hasRight;
// Again get set is same
Groups Class
private int _groupId;
private string _groupName;
private ObservableCollection<Rights> _rights;
// Again, same Get/Set like always
XAML
In my XAML I have:
a ComboBox. ComboBox.ItemsSource bind to GroupsCollection. ComboBox.SelectedValue bind to SingleEmployee.Group.
So while changing the ComboBox, the Group of the Single Employee will be set.
This ComboBox also got an SelectionChanged Event where I set the RightsCollection equal to SingleEmployee.Group.Rights. So that both contains the same items/values now.
It also contains an ItemsControl where I can set the rights myself (and where the rights will be loaded when ComboBox.SelectionChanged (which works).
<ComboBox x:Name="GroupComboBox" ItemsSource="{Binding GroupsCollection}" SelectedValue="{Binding SingleEmployee.Group}" DisplayMemberPath="GroupName" SelectionChanged="GroupComboBox_SelectionChanged">
ItemsControl
<ItemsControl ItemsSource="{Binding RightsCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<DockPanel>
<ToggleButton DockPanel.Dock="Right" Margin="10" IsChecked="{Binding HasRight}"/>
<TextBlock FontSize="15" FontWeight="Bold" Text="{Binding RightName}" DockPanel.Dock="Left" Margin="10" />
</DockPanel>
<TextBlock Text="{Binding RightsDesc}" Margin="30 0 0 10" TextWrapping="Wrap"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
SelectionChanged Event in Code-Behind
Debug.WriteLine("############ SelectionChanged Event ############");
Debug.WriteLine("# Before Change ##");
Debug.WriteLine($"SingleEmployee.Group.Rights.Count: {_viewModel.SingleEmployee.Group.Rights.Count} | RightsCollection.Count: {_viewModel.RightsCollection.Count}");
for (int i = 0; i < _viewModel.SingleEmployee.Group.Rights.Count; i++)
{
Debug.WriteLine($"Name: {_viewModel.SingleEmployee.Group.Rights[i].RightName}, HasRight: {_viewModel.SingleEmployee.Group.Rights[i].HasRight} || Name: {_viewModel.RightsCollection[i].RightName}, HasRight: {_viewModel.RightsCollection[i].HasRight}");
}
_viewModel.RightsCollection = _viewModel.SingleEmployee.Group.Rights;
Debug.WriteLine("# After Change #");
Debug.WriteLine($"SingleEmployee.Group.Rights.Count: {_viewModel.SingleEmployee.Group.Rights.Count} | RightsCollection.Count: {_viewModel.RightsCollection.Count}");
for (int i = 0; i < _viewModel.SingleEmployee.Group.Rights.Count; i++)
{
Debug.WriteLine$"Name: {_viewModel.SingleEmployee.Group.Rights[i].RightName}, HasRight: {_viewModel.SingleEmployee.Group.Rights[i].HasRight} || Name: {_viewModel.RightsCollection[i].RightName}, HasRight: {_viewModel.RightsCollection[i].HasRight}");
}
Debug.WriteLine("########## SelectionChanged Event END ##########");
Debug.WriteLine("################################################");
Set ViewModel in Code-Behind
private readonly EmployeeViewModel _viewModel;
// constructor...
{
_viewModel = (EmployeeViewModel) DataContext;
}
Save Button Command Method
Debug.WriteLine("############## After Button Click ##############");
for (int i = 0; i < RightsCollection.Count; i++)
{
Debug.WriteLine($"Name: {SingleEmployee.Group.Rights[i].RightName}, HasRight: {SingleEmployee.Group.Rights[i].HasRight} || Name: {RightsCollection[i].RightName}, HasRight: {RightsCollection[i].HasRight}");
}
Debug.WriteLine("################################################");
bool equal = RightsCollection.Count == SingleEmployee.Group.Rights.Count && RightsCollection.All(x => SSingleEmployee.Group.Rights.Contains(x));
Debug.WriteLine(equal ? "Nothing Changed" : "Something changed");
What I tried
SelectionChanged Event
// No Success
var collection = new ObservableCollection<Rights>(_viewModel.SingleEmployee.Group.Rights);
_viewModel.RightsCollection = collection;
.
// No Success
foreach(var item in _viewModel.SingleEmployee.Group.Rights)
_viewModel.RightsCollection.Add(item);
Result of the Debugging
SelectionChangedResult
|
SelectionChangedResult
After Button Click
After Button Click
_viewModel.RightsCollection = _viewModel.SingleEmployee.Group.Rights;
the left collection has the same reference of the right collection.
So, if you change one collection it will reflect in the other collection.
ObservableCollection<Whatever> _viewModel.RightsCollection = new ObservableCollection<Whatever>();
foreach(var item in _viewModel.SingleEmployee.Group.Rights)
_viewModel.RightsCollection.Add(item);
I fixed this problem in my SelectionChanged Event deleting this line:
_viewModel.RightsCollection = _viewModel.SingleEmployee.Group.Rights;
and replaced it with this:
for (int i = 0; i < _viewModel.SingleEmployee.Group.Rights.Count; i++)
{
if (_viewModel.SingleEmployee.Group.Rights[i].HasRight != _viewModel.RightsCollection[i].HasRight)
{
_viewModel.RightsCollection[i].HasRight = _viewModel.SingleEmployee.Group.Rights[i].HasRight;
}
}
Because both collections are nearly the same, they will always have the same amount of items so I can use a for-loop.
If a value is not the same, then the value will change.
This way I don't create a reflection (I guess) so it's working.
The only thing now is, that
bool equal = RightsCollection.Count == SingleEmployee.Group.Rights.Count && RightsCollection.All(x => SingleEmployee.Group.Rights.Contains(x));
isn't working but I will use a for-loop here too which checks if the items contains the same value, if not then "Something changed".

How to dynamically link a CheckBox to enable a TextBox in C# (WPF)?

I have a row in a grid with 5 textboxes, 2 of which are enabled by checkboxes. I am trying to dynamically add additional rows to the grid when a button is clicked. The eventhandler I added will only enable the textbox in the first row, but not in the current row (2nd). There is another eventhandler which handles the box in the first row, this is a new one. (BTW I only have part of the second row coded). Not sure if I should try making a template for the checkbox, and then use binding to the textbox? And if so, the instructions I've read on connecting the binding are vague and confusing. Or can I do the binding directly? Or ?
public partial class Window2 : Window
{
int currentColumn = 0;
int currentRow = 1;
int timesCalled = 1;
public Window2()
{
InitializeComponent();
}
private void AddLevelButton_Click(object sender, RoutedEventArgs e)
{
string level = this.Level.Content.ToString(); //label for the row
string[] splitLevel = level.Split(' ');
int levelNum = int.Parse(splitLevel[1]);
levelNum = timesCalled + 1;
int nextRow = currentRow + 1;
int nextColumn = currentColumn + 1;
Label levelLabel = new Label();
levelLabel.Content = "Level " + levelNum.ToString();
Grid.SetRow(levelLabel, nextRow);
Grid.SetColumn(levelLabel, currentColumn);
FlowGrid.Children.Add(levelLabel);
currentColumn++;
CheckBox antesBox = new CheckBox(); //the checkbox to enable the
antesBox.Name = "AntesBox"; //textbox which follows
antesBox.VerticalAlignment = VerticalAlignment.Bottom;
antesBox.HorizontalAlignment = HorizontalAlignment.Right;
antesBox.FontSize = 16;
antesBox.Width = 20;
antesBox.Height = 20;
antesBox.Checked += AntesBox_Checked1; //eventhandler
Grid.SetRow(antesBox, nextRow);
Grid.SetColumn(antesBox, currentColumn);
FlowGrid.Children.Add(antesBox);
nextColumn = ++currentColumn;
TextBox enterAntes = new TextBox(); //the textbox to be enabled
enterAntes.Name = "EnterAntes";
enterAntes.Margin = new Thickness(5, 0, 5, 0);
enterAntes.FontSize = 16;
enterAntes.FontFamily = new FontFamily("Verdana");
enterAntes.IsEnabled = false;
enterAntes.KeyDown += EnterAntes_KeyDown1; //tested; this works
Grid.SetRow(EnterAntes, nextRow);
Grid.SetColumn(EnterAntes, nextColumn);
FlowGrid.Children.Add(EnterAntes);
nextColumn = ++currentColumn;
}
private void enterAntes_KeyDown1(object sender, KeyEventArgs e)
{
int key = (int)e.Key;
e.Handled = !(key >= 34 && key <= 43 ||
key >= 74 && key <= 83 || key == 2);
}
private void AntesBox_Checked1(object sender, RoutedEventArgs e)
{
EnterAntes.IsEnabled = true;
}
You need to add following codes to enable text boxes.
Following is the xaml view of the datagrid.
<DataGrid x:Name="gvTest" AutoGenerateColumns="False" ItemsSource="{Binding}" HorizontalAlignment="Left" Margin="86,204,0,0" VerticalAlignment="Top" Height="132" Width="436">
<DataGrid.Columns>
<DataGridTemplateColumn Header="TextBox 01">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="txt01" Width="50" Text="{Binding TxtBox01}"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="TextBox 02">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="txtbox02" Width="50" Text="{Binding TxtBox02}"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="TextBox 03">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="txtbox03" Width="50" Text="{Binding TxtBox03}"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="TextBox 04">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="txtbox04" Width="50" IsEnabled="False" Text="{Binding TxtBox04}" Loaded="txtbox04_Loaded"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="TextBox 05">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBox x:Name="txtbox05" Text="{Binding TxtBox05}" Loaded="txtbox05_Loaded"></TextBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Enable" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<CheckBox x:Name="chk01" Checked="chk01_Checked" IsChecked="{Binding IsActive}"></CheckBox>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
add the following codes to declare instance of required textboxes and declare observable collection.
TextBox txt04;
TextBox txt05;
ObservableCollection<TestItem> TestItemList = new ObservableCollection<TestItem>();
add the following codes to the loaded event of the required textboxes.
private void txtbox04_Loaded(object sender, RoutedEventArgs e)
{
txt04 = (sender as TextBox);
//txt04.IsEnabled = false;
}
private void txtbox05_Loaded(object sender, RoutedEventArgs e)
{
txt05 = (sender as TextBox);
}
Now, create a model class with following code segment in order to bind values to the datagrid.
public class TestItem
{
public string TxtBox01 { get; set; }
public string TxtBox02 { get; set; }
public string TxtBox03 { get; set; }
public string TxtBox04 { get; set; }
public string TxtBox05 { get; set; }
public bool IsActive { get; set; }
public TestItem()
{
IsActive = false;
}
}
I have used a button to add new rows to the datagrid. add the following codes to the button click to add rows.
private void btnAdd_Click(object sender, RoutedEventArgs e)
{
TestItemList.Add(new TestItem());
gvTest.ItemsSource = TestItemList;
}
Finally, add the following codes to the checkbox checked event
CheckBox c = (sender as CheckBox);
if (c.IsChecked==true)
{
txt04.IsEnabled = true;
txt05.IsEnabled = true;
}
Hope this helps you to fulfill your requirement.
At the risk of perpetuating the wrong approach, it seems to me that the most direct way to address your specific need here is to fix your event handler so that it is always specific to the text box that corresponds to the checkbox in question. This is most easily done by moving the event handler subscription to below the declaration of the local variable enterAntes, and then use that variable in the event handler (i.e. so that it's capture by the anonymous method used as the event handler). For example:
TextBox enterAntes = new TextBox(); //the textbox to be enabled
antesBox.Checked += (sender, e) => enterAntes.IsEnabled = true;
Now, that said, I whole-heartedly agree with commenter Mark Feldman who suggests that the code you've written is not the right way to accomplish your goal in WPF.
I'm not sure I agree with the characterization "harder". That's such a loaded and subjective term, depending in no small part in what you find easy or hard. Being new to WPF, you almost certainly find the concepts of data binding and declarative XAML-based coding "hard", and direct, procedural code such as in your example "easy" (or at least "easier" :) ).
But he's absolutely right that in the long run, you will be better served by doing things "the WPF way". You may or may not wind up with much less code, but the WPF API is designed to be leveraged as much as possible from the XAML, and use code-behind minimally (and certainly not for the purpose to build the UI).
So what's all that mean for your code? Well, I ramble and it would be beyond the scope of a good, concise Stack Overflow answer for me to try to rewrite your entire code from scratch to suit the WPF paradigm. But I will offer some suggestions as to how I'd handle this.
First, forget the UI objects themselves for a moment. Write classes that describe the key characteristics of the UI as you want it to be, without being the UI itself. In this example, this could mean that you should have a list of rows. There should also be a class that defines what a single row looks like, e.g. with a bool property (to reflect the checkbox state) and a string property (to reflect the text box value). This is your "model"; i.e. each class is an individual model class, and at the same time you could consider the entire collection of classes as the model for your UI.
Now, go back to your UI and define it in XAML, not in code. There are several different ways to represent a list in the UI. Classes like ListBox, ListView, DataGrid, or even ItemsControl (the base class for many of the list-oriented controls). Bind the source of your list control to the model list you created in the previous step.
Define a DataTemplate (again, in XAML) for the type of class that is contained in the list. This will declare the UI for a single row in your list. Your template might look something like this:
<!-- Make sure you defined the "local" XML namespace for your project using the
xmlns declaration -->
<DataTemplate DataType="{x:Type local:MyRowModel}">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Text}" IsEnabled={Binding IsEnabled}"/>
<Checkbox Checked="{Binding IsEnabled}"/>
</StackPanel>
</DataTemplate>
All of the XAML inside the DataTemplate element tells WPF what you want a single row to look like, within the control that is presenting your row model. That control will set the DataContext for the list item defined by the template, such that the {Binding...} declarations can reference your row model's properties directly by name.
That row model in turn might look something like this:
class MyRowModel : INotifyPropertyChanged
{
private string _text;
private bool _isEnabled;
public string Text
{
get { return _text; }
set
{
if (_text != value)
{
_text = value;
OnPropertyChanged();
}
}
}
public bool IsEnabled
{
get { return _isEnabled; }
set
{
if (_isEnabled != value)
{
_isEnabled = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName]string propertyName = null)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
When your button to add a new item is clicked, don't mess with the UI directly. Instead, add a new element to your list of rows. Let WPF do the work of updating the UI to match.
NOTES:
The above uses StackPanel for the data template for convenience. If you want things lined up in columns, you'll probably want to use a Grid and declare its columns using SharedSizeGroup.
Or better yet, maybe you can use DataGrid which, assuming its default presentation of the values is acceptable to you, offers simple and automatic handling of exactly this type of layout.
The above is not meant to be anything close to a complete explanation of how to use data templating. It's just supposed to get you pointed in the right direction. Templating is one of WPF's more powerful features, but with that it also has the potential to be fairly complex.
For all of this to work, your types need to provide notification when they change. In the case of the row model, you can see it implements INotifyPropertyChanged. There is also an interface INotifyCollectionChanged; usually you don't have to implement this yourself, as WPF has the type ObservableCollection<T> which you can use just like List<T>, to store lists of data but with a way for notifications of changes to be reported to WPF.
I know this is a lot to take it all at once. Unfortunately, it's not feasible to try to explain all in a single answer all the things you need to learn to get this right. Frankly, even the above is pushing the limits as to what's within the scope of a Stack Overflow answer. But I hope that I've hit just the right highlights to get you looking at the right parts of the WPF documentation, and to understand the underlying philosophy of the WPF API.

How to set focus to a bound ListboxItem by pressing its child element?

I am developping a small WPF application which consist mostly in displaying ObservableCollection<> in others ObservableCollection<>, and so on.
Here is a code example of what my application looks like:
<Listbox Name="MainList" ItemsSource={Binding}>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<Textblock Text={Binding MainName} />
<Button>Add item</Button>
<Button>Delete item</Button>
<Listbox Name="ChildList" ItemsSource="{Binding Path=ChildItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<Textblock Text={Binding ChildName} />
</DataTemplate>
</ListBox.ItemTemplate>
</Listbox>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</Listbox>
And visually it pretty much looks like this:
EDIT:
I will re-explain what I am trying to do.
Whenever I click Button A or Button B I want to Select the MainList ListBoxItem in which they are contained (i.e: A Item)
And in a second time whenever I click Button B:
I want to be sure that a ListBoxItem is selected in ChildList(Second Listbox in the picture)
And if so, I want to delete it in code-behind.
But my main problem is since everything is generated by my bindings I cannot get, so far, an element from my ChildList because ChildList is duplicated in any of my MainList ListBoxItem.
If I understand well the problem is that you want first click on a button of unselected item to select the MainItem, and on next click, when MainItem is already selected, preform click action. Try this when button is clicked:
private ListBoxItem FindItemContainer(DependencyObject obj)
{
while (obj != null && !(obj is ListBoxItem))
{
obj = VisualTreeHelper.GetParent(obj);
}
if (obj != null)
return obj as ListBoxItem;
else
return null;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var lbi = FindItemContainer(sender as DependencyObject);
if (lbi != null)
{
if (lbi.IsSelected)
{
//do click event
}
else
lbi.IsSelected = true;
}
}
Of course you can also do it more MVVM way by binding ListBoxItem.IsSelected to lets say bool MainItem.MyItemIsSelected
<ListBox.ItemContainerStyle>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="IsSelected" Value="{Binding Path=MyItemIsSelected, Mode=TwoWay}"/>
</Style>
</ListBox.ItemContainerStyle>
and Button.Command to your ICommand MainItem.DeleteCommand and then when command is executed do something like that:
if (MyItemIsSelected)
{
//do command body
}
else
MyItemIsSelected = true;
which will be better long term because you could replicate SelectedItem behaviour in ChildList object (add MyItemIsSelected and bind it to inner 'ListBoxItem.IsSelected, like discribed above) and add MySelectedItem property to ChildList:
ChildItem MySelectedItem
{
get
{
return Items.FirstOrDefault(n=>n.MyItemIsSelected);
}
}
and your delete command would look like this:
if (MyItemIsSelected)
{
ChildItem selItem = ChildItems.MySelectedItem;
if (selItem != null) ChildItems.Items.Remove(selItem);
}
else
MyItemIsSelected = true;
if everything is data bound and lists are ObservableCollections then you can do all that in object and UI will follow. Actually you can do only this child selection binding bit and still use first solution and in Button_Click look like this:
private void Button_Click(object sender, RoutedEventArgs e)
{
var lbi = FindItemContainer(sender as DependencyObject);
if (lbi != null)
{
if (lbi.IsSelected)
{
MainItem mainItem = lbi.Content as MainItem;
ChildItem selChild = mainItem.ChildItems.MySelectedItem;
if (selChild != null) mainItem.ChildItems.Items.Remove(selChild);
}
else
lbi.IsSelected = true;
}
}
Here is simple, working example on Dropbox
You can do everything you want to do in code behind:
Find the item on which the Button is pressed: in the click-event, cast the sender parameter to type Button. Its DataContext property will contain the item you want to select.
Select the item: set MainList.SelectedItem to the item.
Focus will be on the Button, but that should be ok, since it is inside the item.
Find the selected item in second listbox: locating the ListBox in the DataTemplate is tricky, but you could set its IsSynchronizedWithCurrentItem property to True, and then use the underlying child collection's default CollectionView. You'd find the current item of MainList like above. Then you'd use:
itemToDelete = CollectionViewSource.GetDefaultView(item.ChildItems).CurrentItem;
item.ChildItems.Remove(itemToDelete);

DataGrid.SelectedItem not bound to currently selected cell

Ran into this issue on several occasions.
When finding SelectedItem or selected column on say right click menu or selecting combo box in a cell. The SelectedItem will be null or the previously selected row.
private void ComboBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) {
// Correct
m_BeginEditString = ((ComboBox)sender).SelectedValue.ToString();
// Wrong. selected item is last selected row, example clicking directly on combobox will not select row, and be null.
m_BeginEditRow = (RowItem)MyDataGrid.SelectedItem;
}
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox SelectedItem="{Binding myItem, Mode=TwoWay,
NotifyOnSourceUpdated=True, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding Source={StaticResource enum}}"
SelectionChanged="ComboBox_Changed"
LostKeyboardFocus="ComboBox_LostKeyboardFocus"
GotKeyboardFocus="ComboBox_GotKeyboardFocus" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
Solved by doing it a completely different way, thanks #Ramesh Muthiah for direction:
private void ComboBox_Changed(object sender, SelectionChangedEventArgs e) {
if (((ComboBox)sender).IsLoaded) { // disregard SelectionChangedEvent fired on population from binding
if (e.RemovedItems.Count != 0) {
for (Visual visual = (Visual)sender; visual != null; visual = (Visual)VisualTreeHelper.GetParent(visual)) { // Traverse tree to find corred selected item
if (visual is DataGridRow) {
DataGridRow row = (DataGridRow)visual;
m_BeginEditRow = new MyRowItem((MyRowItem)row.Item); // Copy constructor, otherwise passed by reference
break;
}
}
MyEnum newItem = (MyEnum)e.AddedItems[0];
MyEnum oldItem = (MyEnum)e.RemovedItems[0];
if (m_BeginEditRow.Combo1 == newItem) {
m_BeginEditRow.Combo1 = oldItem;
} else {
m_BeginEditRow.Combo2 = oldItem;
}
DoStuff(m_BeginEditRow, false);
}
}
}
Instead of accessing the selected Item directly, you can access through the parent object and try to access whatever you want. This is alternative approach. I hopes this helps you
Combobox objMyButton = null;
if (sender is Combobox)
{
objMyButton = (sender as Combobox );
}
//You can access the parent object which means corresponding DataGridRow and do whatever you want
for (var vis = sender as Visual; vis != null; vis = VisualTreeHelper.GetParent(vis) as Visual)
if (vis is DataGridRow)
{
var row = (DataGridRow)vis;
break;
}

Categories

Resources