WinForms ListView empty rows glitch - winforms

I have a glitch in a WinForms C# ListView (with custom modifications to sort and filters on all columns, but it happened also in a standard ListView).
I modify the ListView Items with this (fairly standard) pattern:
BeginUpdate();
// add some items
// remove some other items
Sort();
EndUpdate();
But if I call this code when the ListView is already scrolled, then I get some empty (non selectable) rows before the real items, and 2 scrollabars even if they are not needed.
It looks like a graphic glitch, because when I scroll the list then the empty items disappear.
Have anyone met this problem before?

Ok, I found the problem. A call to set a column Width = -2 during Resize was messing the owner-draw filters...

This ListView graphic bug. A similar problem can be reproduced, if while changing the ListView size, change the width of its columns. As a solution, the method proposes to change the width in a separate thread.
private void ListView_SizeChanged(object sender, EventArgs e)
{
var widthChangedThread = new Thread(() => SetNewColumnSize()) {IsBackground = true};
widthChangedThread.Start();
}
private void SetNewColumnSize()
{
Invoke(new MethodInvoker(() =>_columnHeader.Width += 10));
}

This control behave in a strange way but setting up the scrollabe
property to false in the resize event fixed completly the problem as:
With DirectCast(sender, ListView)
'do not allow scrolling in the resize event
'ortherwise there is a condition where the control
'stop showing the row data
IsScrollEnabled = .Scrollable
.Scrollable = False
ThisColumnHeader = .Columns("colMessage")
If ThisColumnHeader IsNot Nothing Then
.BeginUpdate()
'With .Columns("colMessage")
' .Width = -2
'End With
If .Items.Count > 0 Then
'If MyListDownloadMessage.Count > 0 Then
If ToolStripSerialRxFillDown.Checked Then
.EnsureVisible(.Items.Count - 1)
Else
If .TopItem IsNot Nothing Then
.EnsureVisible(.TopItem.Index)
End If
End If
End If
.EndUpdate()
.Refresh()
End If
.Scrollable = IsScrollEnabled
End With

Related

Why is the SelectedItem property of my DataGrid subclass set to Nothing?

I have subclassed the WPF DataGrid in my VB.NET application because I will need to use this component frequently but also need to have some extra features, in this case adding a new row when the tab key is pressed on the bottom right cell.
I have Overidden the OnKeyDown event of the base class. This is being triggered when the tab keys is pressed while the grid is focused, exactly as I want. However, when the event is triggered I need to be able to determine whether I'm on the bottom-right cell or not. To do this I'd like to get the SelectedItem property of my DataGrid and use that to determine which cell is selected.
I am doing all of this programmatically because I don't want users to have to write any more XAML than they would have to for a regular datagrid. It should function in exactly the same way except if you tab on the bottom right cell a new row is added and the first cell of that row is selected. this should apply no matter how many rows/columns the user has in the datagrid.
The code below shows what I want to do but the SelectedItem is not set.
Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
MyBase.OnKeyDown(e)
If e.Key = Key.Tab Then
Dim colIndex As Integer = Me.Columns.IndexOf(Me.CurrentColumn)
Dim colCount As Integer = Me.Columns.Count - 1
If -1 = colIndex Then
'the next line throws a System.NullReferenceException because SelectedItem is not set
If SelectedItem.Equals(Items(Items.Count - 1)) Then
Focus()
Dim dgrCI = New DataGridCellInfo(Items(Items.Count - 1), Columns(colIndex))
ScrollIntoView(Items(Items.Count - 1))
BeginEdit()
End If
End If
End If
End Sub
The SelectedItem property should be set to the last item in the table but is instead set to Nothing. Why is this?
Edit:
The answer from Ppp is correct. it seems that by the time the OnKeydown event is triggered the DataGrid has already lost focus. I resolved this issue by using the OnPreviewKeydown event instead.
When your tab key is pressed in the last cell you basically tab out of the datagrid which is why I am guessing your SelectedItem is null.

Maintain scroll position on updating the ItemSource of a silverlight datagrid

I'm using a DataGrid in my silverlight application to display some data that's refreshed on a timer. My problem is that when this happens the vertical scrollbar in the grid resets to the top, whereas I want it to stay in the same position. Does anyone know how I can make this happen?
I've tried overriding the ItemsSource property on the grid to store the vertical scroll position and then reset it, but this only affects the scrollbar and doesn't force the correct rows to be displayed. Is there a way to force this behaviour?
Here is a similar question about Setting the scroll bar position on a ListBox
After rebinding Silverlight Listbox control how do you get it listbox to scroll to back to the top?
Since the DataGrid also supports a ScrollIntoView method, you should be able to use a similar technique such as
theDataGrid.ItemsSource = data;
theDataGrid.UpdateLayout();
theDataGrid.ScrollIntoView(theDataGrid.SelectedItem, theDataGrid.Columns[0]);
I couldn't find a decent answer last time I looked. I wanted to keep the current element selected in the grid but that wouldn't work on an ICollectionView refresh (I use MVVM and get automatic updates from the server).
ScrollIntoView() was not an option for me because the currently selected item may NOT be in view. Having the CurrentChanged event firing out of control was also quite a bother.
In the end, I used the Infragistics grid and it does just that out of the box. Problem solved for me.
You may have a look at the DevExpress free grid. I think it had the same nice behaviour (I tested it but I can't remember the outcome).
You could try setting the SelectedItem thro the UI thread, so that the UI can refresh itself,
like so
private void Button_Click(object sender, RoutedEventArgs e)
{
Person p = new Person() { Name="sss",Age=11}; //datagird's itemsSource is Collection<person>
people.Add(p);
dg.SelectedItem = p; //dg is my datagrid name
Dispatcher.BeginInvoke(() => { dg.SelectedItem = p; });
}
Im assuming that new rows are loaded thro the ViewModel, so thats why it makes sense to place the BeginInvoke there. Since the ViewModel operations run on a different thread, and just setting the SelectedItem on its own might not work, this has worked for someone else
I've also had issues with this. I solved it by remembering the item I want to scroll to, then re-binding the DataGrid. I handle the LayoutUpdated event in order to implement the desired functionality:
void MyDataGrid_LayoutUpdated(object sender, EventArgs e)
{
// Reference the data item in the list you want to scroll to.
object dataItem = yourDataItem;
// Make sure the item is not null and didn't already scroll to the item.
if (dataItem != null && this.dataItemScrolledTo != dataItem)
{
// Remember the item scrolled to.
this.dataItemScrolledTo = dataItem;
// Scroll datagrid to the desired item.
MyDataGrid.ScrollIntoView(dataItem, MyDataGrid.Columns[0]);
}
}
I've modified CodeMaster's solution so that you don't need a class level variable. Put this code in the method that updates the ItemsSource. It will dynamically create the eventhandler, attach it, then detach it.
EventHandler MyDataGrid_LayoutUpdated = null;
MyDataGrid_LayoutUpdated = (s, e) =>
{
MyDataGrid.ScrollIntoView(dataItem, MyDataGrid.Columns[0]);
MyDataGrid.LayoutUpdated -= MyDataGrid_LayoutUpdated;
};
MyDataGrid.LayoutUpdated += MyDataGrid_LayoutUpdated;

How can I put a Silverlight 3 DataGridCell into edit mode in code?

I want to be able to pick a specific cell in a Silverlight 3.0 DataGrid and put it into edit mode. I can use the VisualTreeManager to locate the cell. How do I switch to edit mode?
Each DataGridCell looks like this in the VisualTreeManager:
System.Windows.Controls.DataGridCell
System.Windows.Controls.Grid
System.Windows.Shapes.Rectangle
System.Windows.Controls.ContentPresenter
System.Windows.Controls.TextBlock
System.Windows.Shapes.Rectangle
System.Windows.Shapes.Rectangle
with the TextBlock containing the text I want to edit.
Update
Following #AnthonyWJones' suggestion, here's how I tried to do this using BeginEdit().
I wanted to keep it simple so I thought I'd pick a column in the first row. Even that proved beyond my SL knowledge! In the end, I get the first row by creating a field called firstRow to hold it:
private DataGridRow firstRow;
added a LoadingRow handler to the DataGrid:
LoadingRow="computersDataGrid_LoadingRow"
and
private void computersDataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
if (this.firstRow == null)
this.firstRow = e.Row;
}
and then adding a button to the panel to trigger the edit:
private void Button_Click(object sender, RoutedEventArgs e)
{
this.dataGrid.SelectedItem = this.firstRow;
this.dataGrid.CurrentColumn = this.dataGrid.Columns[4];
this.dataGrid.BeginEdit();
}
I click the button and the correct cell is selected but it doesn't go into edit on the cell. It takes a manual click to achieve that.
I'm not sure why you need to find the DataGridCell using VisualTreeManager nor do I know currently how you would properly start editing . You may get away with simply setting the cell's visual state to editing.
VisualStateManager.GoToState(myDataGridCell, "Editing", true);
I'm not sure how the grid behaves when you do something like the above. You may find things goe a bit pearshaped if you need DataGrid to help you revert changes to a row.
The "standard" approach would be to set the DataGrid SelectedItem property to the item represented by the row, set the CurrrentColum property to the DataGridColumn object that represents to the column in which the cell is found. Then call the BeginEdit method.
I am not able to understand your problem properly, but I had a similar problem
I wanted to make only few of the Grid Cells editable and rest were not. Instead of creating a logic and assigning ReadOnly as true/ false, I did the simple thing.
Mark the whole Grid's cells are writable, IsReadOnly as false
Set the event PreparingCellForEdit and send a callback
When you double click on a cell, it gets in the edit mode
Check whether this cell you want to be editable
If it is allowed to be edited, go ahead
If that cell is ReadOnly, then call CancelEdit
The sample code goes like
namespace foo
{
public class foobar
{
public foobar()
{
sampleGrid = new DataGrid();
sampleGrid.IsReadOnly = false;
sampleGrid.PreparingCellForEdit += new EventHandler<DataGridPreparingCellForEditEventArgs>(sampleGrid_PreparingCellForEdit);
}
void sampleGrid_PreparingCellForEdit(object sender, DataGridsampleGrid_PreparingCellForEditEventArgs e)
{
if (sampleGrid.SelectedItem != null)
{
bool isWritableField = CheckIfWritable()
if (isWritableField == false)
{
sampleGrid.CancelEdit();
}
// continue with your logic
}
}
private DataGrid sampleGrid;
}
}

WPF ListView Databound Drag/Drop Auto Scroll

I've been working with Bea's solution here for a while and finding it very helpful. Problem now I'm having is when I drag-n-drop items within or to another ListView control and I want to scroll up/down "during" the drag (moving an item from index 30 to index 1), it's not happening. I would have to drag to the top of the visual items in the ListView, manually scroll up, then drag again, eventually ending at the position I want. This isn't very user friendly.
Now I found the function (DragDropHelper.DropTarget_PreviewDragOver) that I would want to do the testing of which item is being dragged over, and I'm getting that.
Dim pt As Point = e.GetPosition(DirectCast(Me.targetItemsControl, UIElement))
' Perform the hit test against a given portion of the visual object tree.
Dim result As HitTestResult = VisualTreeHelper.HitTest(Me.targetItemsControl, pt)
Now from there I can get the DependencyProperty of this visual hit
Dim lvi As ListViewItem = TryCast(GetDependencyObjectFromVisualTree(TryCast(result.VisualHit, DependencyObject), GetType(ListViewItem)), ListViewItem)
Which is of a ListViewItem. Now in the function DropTarget_PreviewDragOver I have the "DraggedItem" which is of type Picture in Bea's example, but that can change depending on the ObservableCollection you have bound to the ListView. Now, I want to drag the ListView up or down depending on where the mouse is on the control. I've attempted with the below un-finished non-working code
If lvi IsNot Nothing Then
If pt.Y <= 25 Then
Dim lv As ListView = TryCast(targetItemsControl, ListView)
If lv IsNot Nothing Then
Dim index As Integer = lv.Items.IndexOf(lvi)
If index > 1 Then
lv.ScrollIntoView(lv.Items(index - 1))
End If
End If
Else
If pt.Y >= Me.targetItemsControl.ActualHeight - 25 Then
Debug.Print("Scroll Down")
End If
End If
End If
Can someone point me in the right direction to get this ItemsControl or ListView to scroll when dragging over the items??
Thanks!
I'm still messing around with this exact same issue too. I'm using a slightly modified version of Bea's Drag and Drop found here, which is in VB instead of C#. When I used ScrollIntoView as described above, I could scroll down but not up. So I messed around and came up with this as my DropTarget_PreviewDragOver:
Private Sub DropTarget_PreviewDragOver(ByVal sender As Object, ByVal e As DragEventArgs)
Dim draggedItem As Object = e.Data.GetData(Me.m_format.Name)
Me.DecideDropTarget(e)
If (Not draggedItem Is Nothing) Then
If (TypeOf m_targetItemsControl Is ListBox) Then
Dim lb As ListBox = CType(m_targetItemsControl, ListBox)
Dim temp As Integer = m_insertionIndex
Dim scroll As ScrollViewer = Utilities.GetScrollViewer(lb)
If scroll.VerticalOffset = temp Then
temp -= 1
End If
If temp >= 0 And temp <= (lb.Items.Count - 1) Then
lb.ScrollIntoView(lb.Items(temp))
End If
End If
Me.ShowDraggedAdorner(e.GetPosition(Me.m_topWindow))
Me.UpdateInsertionAdornerPosition()
End If
e.Handled = True
End Sub
and I had to include this utility function, taken from here
Public Shared Function GetScrollViewer(ByVal listBox As ListBox)
Dim scroll_border As Decorator = CType(VisualTreeHelper.GetChild(listBox, 0), Decorator)
If (TypeOf scroll_border Is Decorator) Then
Dim scroll As ScrollViewer = CType(scroll_border.Child, ScrollViewer)
If (TypeOf scroll Is ScrollViewer) Then
Return scroll
Else
Return Nothing
End If
Else
Return Nothing
End If
End Function
which is great and all. Then running out what theuberk mentioned above with the adorner moving, and in the spirit of making this easy for someone else later, I added a variable to the DragDropAdorner class:
Private m_mouseDelta As Point
Added this to the last line of DragSource_PreviewMouseLeftButtonDown:
Me.m_mouseDelta = e.GetPosition(m_sourceItemContainer)
And turned ShowDraggedAdorner into:
Private Sub ShowDraggedAdorner(ByVal currentPosition As Point)
If (Me.m_draggedAdorner Is Nothing) Then
Dim adornerLayer As AdornerLayer = adornerLayer.GetAdornerLayer(Me.m_topWindow.Content)
Me.m_draggedAdorner = New DraggedAdorner(Me.m_draggedData, DragDropBehavior.GetDragTemplate(Me.m_sourceItemsControl), m_topWindow.Content, adornerLayer)
End If
Me.m_draggedAdorner.SetPosition((currentPosition.X - m_mouseDelta.X), (currentPosition.Y - m_mouseDelta.Y))
End Sub
What I did was took advantage of the ListBox.ScrollIntoView method. Basically, when you update your drop target, you can just call this method on it and wpf will do all the work. All you need to know is the index of the drop target item. This handles both vertical and horizontal scrolling.
this.listView.ScrollIntoView(this.listView.Items[index]);
When you use this method, your adorner might move with the scrolling ListBox. To fix this, I just set my adorner parent and adorner layer parent to the content of the window at the top of the visual tree (i.e. topWindow.Content).
Another possibility to scroll is to use the ScrollBar-Commands. You can do this without climbing down the VisualTree. If you have a styles ListBox with no border the GetScrollViewer()-Method would not work anymore.
I use the first Element of the ItemContainerGenerator as CommandTarget for the ScrollBar.LineXXXCommand:
Point p = e.GetPosition(itemsControl);
IInputElement commandTarget = itemsControl.ItemContainerGenerator.ContainerFromIndex(0) as IInputElement;
if (commandTarget != null)
{
if (p.Y < OFFSET_TO_SCROLL)
ScrollBar.LineUpCommand.Execute(null, commandTarget);
else if (p.Y > itemsControl.ActualHeight - OFFSET_TO_SCROLL)
ScrollBar.LineDownCommand.Execute(null, commandTarget);
if (p.X < OFFSET_TO_SCROLL)
ScrollBar.LineLeftCommand.Execute(null, commandTarget);
else if (p.X > itemsControl.ActualWidth - OFFSET_TO_SCROLL)
ScrollBar.LineRightCommand.Execute(null, commandTarget);
}
Calling the LineXXXCommands is similiar to clicking the Arrow-Buttons of a ScrollBar: The ScrollViewer scrolles by a certain ammount which you can configure by setting the 'SmallAmount' Property of the ScrollBar.

Reordering of controls within a flow layout panel

I'm having trouble using the flowlayoutPanel in a C# winform application. What I basically have is a flow layout panel that has 3 sections.
Section #1 is a set of 2 controls .. two dropdown controls, they are always in the same order, always visible in all instances
Section #2 is a set of 5 different controls ... based on a series of factors, 1 of the 5 controls is made visible, all others have the Visible propert set to false
Section #3 is a set of 3 controls .. like Section #1 they are always in the same order and always visible.
So what this boils down to is that Section #2 is variable, the others are static.
The problem comes with Section #2 ... when I change the visibility of any of the controls they appear just fine (I.E. ... Section 1 then Section 2 then Section 3) ... EXCEPT when I set the combobox control to be Visible .... in that case, and ONLY in that case .. the order becomes (Section 1 then Section 3 then Section 2) ... I can't figure out what would cause the ordering to be out of sync in just that case.
What I basically do at the beginning of my method is set ALL controls to Visible = false ... then I set Section 1 Visible = true ... then loop through the conditions of Section 2 and set the appropriate controls Visible = true and finally set Section 3 controls Visible = true.
Does anyone have any experience with the flow layout panel control ordering? I can't figure out what is happening for the ComboBox.
Inside FlowLayoutPanel.Controls is a method function called SetChildIndex(Control c, int index) which allows you to set an object to a specific index.
Since FlowLayoutPanel uses control's indices to determine which order to draw them in, you can set this to whichever control's index you are wanting to swap with, and it will bump that controls index up by one, and every one after that.
Here is snippet from my blog of reordering PictureBoxes in a FlowLayoutPanel.
Add a FlowLayoutPanel on a WinForm named flowLayoutPanel1:
public partial class TestForm: Form
{
public TestForm()
{
InitializeComponent();
this.flowLayoutPanel1.AllowDrop = true
}
private void AddImageToBlog(System.Drawing.Image image)
{
PictureBox pbox = new PictureBox();
pbox.SizeMode = PictureBoxSizeMode.Zoom;
pbox.Height = (_picturebox_height * _ScaleFactor);
pbox.Width = (_picturebox_width * _ScaleFactor);
pbox.Visible = true;
pbox.Image = image;
pbox.MouseDown += new MouseEventHandler(pbox_MouseDown);
pbox.DragOver += new DragEventHandler(pbox_DragOver);
pbox.AllowDrop = true;
flpNewBlog.Controls.Add(pbox);
}
void pbox_DragOver(object sender, DragEventArgs e)
{
base.OnDragOver(e);
// is another dragable
if (e.Data.GetData(typeof(PictureBox)) != null)
{
FlowLayoutPanel p = (FlowLayoutPanel)(sender as PictureBox).Parent;
//Current Position
int myIndex = p.Controls.GetChildIndex((sender as PictureBox));
//Dragged to control to location of next picturebox
PictureBox q = (PictureBox) e.Data.GetData(typeof(PictureBox));
p.Controls.SetChildIndex(q, myIndex);
}
}
void pbox_MouseDown(object sender, MouseEventArgs e)
{
base.OnMouseDown(e);
DoDragDrop(sender, DragDropEffects.All);
}
}
Might it be easier to drop another flowlayoutpanel in for section 2, then drop your section 2 controls into that? That way, the visible controls in your top panel never change and you won't have to worry about ordering.
You can reorder controls on flowpanel, changing parent property of controls and reassigning parent property with the order that you need.
Try this generic solution where you can sort you controls according to a property in the user control.
// When adding and removing controls, the order is not kept.
var runsOrderedByStartDate = this.nodesFlowLayoutPanel.Controls.Cast<RunNodeControl>().Select(_ => new { StartDate = _.StartDateTime, RunControl = _ }).OrderBy(_ => _.StartDate).ToList();
// Sets index of controls according to their index in the ordered collection
foreach (var anonKeyValue in runsOrderedByStartDate)
{
this.nodesFlowLayoutPanel.Controls.SetChildIndex(anonKeyValue.RunControl, runsOrderedByStartDate.IndexOf(anonKeyValue));
}
SetChildIndex does not reset the order of the controls in the flowlayout panel. So when we perform FlowLayoutPanel.GetNextControl(q, true) the output is not correct.
For basic control ordering, the simplest way to control the order of controls in the flowlayoutPanel is to set the flowlayoutPanel TabStop property to true. Set the controls tabstop property to True and set the tab order to the order in which you want the controls to appear.

Resources