DevExpress XtraGrid FocusedRowChanged event problem when changing datasource - devexpress

This problem has bugged me for several years and maybe someone here knows a simple solution, since I just ran into it again.
QUESTION: Is there any way to get the XtraGrid to "forget" the current focused row index before a new (different) datasource is assigned to the grid?
BACKGROUND
We use the XtraGrid as a kind of controller for what is displayed in another panel of a multipane Winform.
Now imagine a hypothetical scenario where the datasource of the XtraGrid keeps changing according to menu selections. Menu item 1 populates the grid with a list of today's entrees in the cafeteria: Id, Name. Menu item 2 populates the grid with a list of Customers the user must phone that day: ID, Name. Important thing is that these are separate distinct datasources, and the grid's datasource is being assigned and reassigned.
CRITICAL FACT FOR THIS QUESTION:
We want the grid's FocusedRowChanged event to be the single place where we trap the user's selection in the controller grid. We are a "no spaghetti code" shop. FocusedRowChanged is better than a click event because it handles keyboard navigation too. The row with the focus contains the ID of the detail record we need to fetch from the database for display in Panel #2. This works--most of the time.
Here's how it doesn't work: let's say that on a given day, the list of customers the user must contact contains only one row. So the first (and only) row in the grid is the focused row. Now let's say that the user goes up to the menu and selects the menu item to display the day's cafeteria entrees. When the user clicks on the first item in the Entrees list, the FocusedRowChanged event does NOT fire because the grid has retained a memory of the focused row index from the previous datasource. The focused row index has not changed. And thus the user's selection doesn't trigger anything.
I tried to get DevExpress to offer a second more row-object-oriented mode (as distinct from row-index-oriented approach) whereby each row in the grid would have a GUID, and the FocusedRowChanged event would fire whenever the GUID of the currently focused row differed from the GUID of the previously focused row, regardless of whether the focused row index happened to be the same. This would allow dynamic changes of datasource and enable the desired behavior. But they demurred.
So I'll ask my question again, Is there any way to get the XtraGrid to "forget" the current focused row index before a new datasource is assigned to the grid?

Tim, I had the exact same problem when the grid only had one row of data in it and then changed data sources. I solved it by setting the gridview.FocusedRowHandle = -1 after setting the new datasource.

In a similar situation, I am subscribing to the
FocusedRowObjectChanged
event (using DevExpress 16.1).

I think that the best solution to this problem is to create a new GridView object and override its DoChangeFocusedRowInternal method. Below you will find the default implementation of this method. All you need to do is to change the marked row just as your needs dictate. Also, take a look at the How to create a GridView descendant class and register it for design-time use article, it contains some useful information.
public class MyGridView : GridView {
protected override void DoChangeFocusedRowInternal(int newRowHandle, bool updateCurrentRow) {
if(this.lockFocusedRowChange != 0) return;
if(!IsValidRowHandle(newRowHandle))
newRowHandle = DevExpress.Data.DataController.InvalidRow;
if(FocusedRowHandle == newRowHandle) return; // <<<<<<
int currentRowHandle = FocusedRowHandle;
BeginLockFocusedRowChange();
try {
DoChangeFocusedRow(FocusedRowHandle, newRowHandle, updateCurrentRow);
}
finally {
EndLockFocusedRowChange();
}
RaiseFocusedRowChanged(currentRowHandle, newRowHandle);
}
}
UPDATE
My code:
namespace MyXtraGrid {
public class MyGridControl : GridControl {
protected override BaseView CreateDefaultView() {
return CreateView("MyGridView");
}
protected override void RegisterAvailableViewsCore(InfoCollection collection) {
base.RegisterAvailableViewsCore(collection);
collection.Add(new MyGridViewInfoRegistrator());
}
}
public class MyGridViewInfoRegistrator : GridInfoRegistrator {
public override string ViewName { get { return "MyGridView"; } }
public override BaseView CreateView(GridControl grid) {
return new MyGridView(grid as GridControl);
}
}
public class MyGridView : GridView {
public MyGridView(GridControl ownerGrid) : base(ownerGrid) { }
public MyGridView() { }
protected virtual bool RowEqual(int focusedRowHandle, int newRowHandle) {
if(IsDesignMode)
return focusedRowHandle == newRowHandle;
DataRow row1 = GetDataRow(focusedRowHandle);
DataRow row2 = GetDataRow(newRowHandle);
return row1 == row2;
}
protected override void DoChangeFocusedRowInternal(int newRowHandle, bool updateCurrentRow) {
if(this.lockFocusedRowChange != 0) return;
if(!IsValidRowHandle(newRowHandle))
newRowHandle = DevExpress.Data.DataController.InvalidRow;
if(RowEqual(FocusedRowHandle, newRowHandle))
return;
int currentRowHandle = FocusedRowHandle;
BeginLockFocusedRowChange();
try {
DoChangeFocusedRow(FocusedRowHandle, newRowHandle, updateCurrentRow);
}
finally {
EndLockFocusedRowChange();
}
RaiseFocusedRowChanged(currentRowHandle, newRowHandle);
}
}
}

You can subscribe on the DataSourceChanged event which will fire when Data source changes (you guessed it!) so then you can get using GetFocusedObject() the object and display the relevant items for the other grid...

Related

Disable Conditional Formatting for specific column

Starting from WinForms Data Grid v14.2 by enabling the GridOptionsMenu.ShowConditionalFormattingItem property, the Conditional Formatting feature becomes available.
By doing a right click on any column header, the Conditional Formatting menu item is showed up, allowing end-users to apply conditional formatting to grid columns.
My question is that is it possible to disable the feature for a specific column? I'm thinking of having the menu item grayed out, or simply not having it (by hiding it somehow) in the list of items.
I'm aware of the fact that the cells of a specific column can be formatted by the conditional formattings put on other columns by applying the formatting to the entire row. But, my goal is only to make sure the user can not access the functionality for a specific column.
You can remove the corresponding menu-item using the GridView.PopupMenuShowing event:
using System.Windows.Forms;
using DevExpress.XtraGrid.Localization;
using DevExpress.XtraGrid.Menu;
using DevExpress.XtraGrid.Views.Grid;
namespace WindowsFormsApplication2 {
public partial class Form1 : Form {
public Form1() {
InitializeComponent();
gridView1.PopupMenuShowing += gridView1_PopupMenuShowing;
}
void gridView1_PopupMenuShowing(object sender, PopupMenuShowingEventArgs e) {
var columnMenu = e.Menu as GridViewColumnMenu;
if(columnMenu != null && columnMenu.Column == this.gridColumn1) {
var conditionalFormattingItem = e.Menu.Items.FirstOrDefault(x => object.Equals(x.Tag, GridStringId.MenuColumnConditionalFormatting));
if(conditionalFormattingItem != null)
conditionalFormattingItem.Visible = false;
}
}
}
}

How to select an item of mx:ComboBox on the basis of substring entered through keyboard

I am using <mx:ComboBox /> and I want to select a matching item on the basis of string entered through keyboard. Currently, <mx:ComboBox /> selects the first matching item based on the first character only. I want this functionality to be customized. I am unable to find that KeyboardEvent listener which does the matching so that I can override it.
To do this yourself, you should look at the following bits and pieces of code below from the ComboBox and ListBase classes. ListBase is what the ComboBox component uses for it's drop down list.
The ComboBox appears to be deferring the keyboard input to the drop down list. It then listens for events from the drop down list to know when the selection has changed (as a result of keyboard or mouse input).
Flex components usually override a method called keyDownHandler() to process the keyboard input when they have focus. Starting there, we come across ComboBox line 2231:
// Redispatch the event to the dropdown
// and let its keyDownHandler() handle it.
dropdown.dispatchEvent(event.clone());
event.stopPropagation();
So now the keyDownHandler() in the drop down list will get executed. That method has a giant switch statement, where the default case statement on line 9197 of ListBase looks like this:
default:
{
if (findKey(event.charCode))
event.stopPropagation();
}
This is where the drop down list decides what to select based on keyboard input (when the input is not an arrow key or page up, etc.). The protected findKey() method simply calls the public findString() method to do this work.
So to override this behavior yourself:
extend the ListBase class and override the findKey() or findString() methods with your custom logic
extend ComboBox class and override the createChildren() method so you can instantiate your custom ListBase class instead of the default one.
Here is the class which I've used in order to make it work. searchStr is user inputted string which needs to be matched. If no dataprovider item gets matched to the searchStr, the overridden listener falls back to the default behaviour. I am using Timer to flush the inputted searchStr after 2 seconds. The possible drawback is that it is assuming the dataprovider to be a collection of String values. But you can modify it accordingly as the need may be.
public class CustomComboBox extends ComboBox
{
private var searchStr:String="";
private var ticker:Timer;
public function CustomComboBox()
{
super();
ticker = new Timer(2000);
ticker.addEventListener(TimerEvent.TIMER, resetSearchString);
}
override protected function keyDownHandler(event:KeyboardEvent):void
{
super.keyDownHandler(event);
// code to search items in the list based on user input.
// Earlier, the default behavior shows the matched items in the dropdown, based on first character only.
// user input is invisible to user.
if((event.charCode>=0x20 && event.charCode<=0x7E) || event.charCode==8) //Range of printable characters is 0x20[space] to 0x7E[~] in ASCII. 8 is ASCII code of [backspace].
{
ticker.reset();
ticker.start();
if(event.charCode==8)
{
if(searchStr=="")
return;
searchStr = searchStr.substr(0, searchStr.length-1);
}
else
{
searchStr += String.fromCharCode(event.charCode);
searchStr = searchStr.toLowerCase();
}
for each(var str:String in dataProvider)
{
if(str.toLowerCase().indexOf(searchStr, 0)>-1)
{
this.selectedItem = dropdown.selectedItem = str;
dropdown.scrollToIndex(dropdown.selectedIndex);
break;
}
}
}
}
/**
* reset the search string and reset the timer.
**/
private function resetSearchString(evt:TimerEvent):void
{
searchStr = "";
ticker.reset();
}
}

Strange listbox behavior. New item won't show unless another item is added or page reloaded by exiting and returning

Here is what a portion of my screen looks like:
The user can pick a choice from the drop-down list and click the add button. Here is the code for the add button:
protected void btnModuleAdd_Click(object sender, EventArgs e)
{
var selectedModule = ddlModsList.SelectedItem.ToString();
var graphicName = this.GraphicName;
var xr = new GraphicModuleXRef();
xr.GraphicName = graphicName;
xr.Module = selectedModule;
// Take drop down list selection and add it to GraphicModuleXRef table.
var context = new XRefDataContext();
context.GraphicModuleXRefs.InsertOnSubmit(xr);
context.SubmitChanges();
}
Basically, it's taking the user's choice and writing it out to a table. This part works fine.
In my Page_Load, I check whether IsPostback and, if it is, I run the code below:
private void LoadOtherModulesUsed()
{
if (this.GraphicName != null)
{
lbModules.Items.Clear();
var context = new XRefDataContext();
var q = context.GraphicModuleXRefs
.Where(a => a.GraphicName.Contains(this.GraphicName));
foreach (GraphicModuleXRef gr in q)
{
lbModules.Items.Add(new ListItem(gr.Module.ToString()));
}
}
}
This code reads from a table, finds all records that match the criteria, and adds them to the listbox.
So, what I'm expecting to happen is for the page to reload and the listbox to be repopulated, including the new entry just added to the table. But, that isn't happening. The screen refreshes like it has reloaded, but the entry doesn't appear in the listbox. However, it IS there, it just can't be seen. If the user adds another entry, by clicking the Add button, the list 'rolls up' one row and the previous entry can be seen. But, not the new one. If the user exits from the screen and re-enters, all the entries in the listbox can be seen. It's almost like the listbox is too short to display all records, but I've tried different heights, with no difference.
I'm wondering if anyone can point me in the right direction?
Put simply, when adding a new item to the listbox, it isn't immediately visible unless another item is added, thereby 'rolling' the list up. Even scrolling the list with the scrollbar doesn't show the new entry until another entry is added. And, if you scroll the list up, you can see the prior entry. So strange!
EDIT: Trying to describe this more simply:
User adds item to listbox by pressing Add button.
New item does not appear in listbox.
User adds another item to listbox by press Add button.
Prior item now shows in listbox if user scrolls listbox up.
The newest item just added, however, does not appear unless step 3 is repeated.
Also, exiting the page and then coming back in loads every item in the list and all is visible.
This is a timing issue. Whats happening is Page_Load runs first in this case and THEN the Click event handler so effectively the control has been bound before the new entry is added. Thats why you're always one refresh behind. Id refactor your code like this so everything runs in the correct order! To understand the timing of event execution I strongly recommend reading this article on MSDN its AWESOME and will really help you get the best from ASP.NET.
Additionally reading this article on MSDN (Also awesome) especially the section on ViewState will explain how the DropDown retains its details even when, in the modified code, you're onlly filling it when the page is NOT a postback and when the click event is fired.
Hope this helps!
public void Page_Load(object sender, EventArgs e)
{
if (IsPostBack) return;
LoadOtherModulesUsed();
}
private void LoadOtherModulesUsed()
{
if (this.GraphicName != null)
{
lbModules.Items.Clear();
var context = new XRefDataContext();
var q = context.GraphicModuleXRefs
.Where(a => a.GraphicName.Contains(this.GraphicName));
foreach (GraphicModuleXRef gr in q)
{
lbModules.Items.Add(new ListItem(gr.Module.ToString()));
}
}
}
protected void btnModuleAdd_Click(object sender, EventArgs e)
{
var selectedModule = ddlModsList.SelectedItem.ToString();
var graphicName = this.GraphicName;
var xr = new GraphicModuleXRef();
xr.GraphicName = graphicName;
xr.Module = selectedModule;
// Take drop down list selection and add it to GraphicModuleXRef table.
var context = new XRefDataContext();
context.GraphicModuleXRefs.InsertOnSubmit(xr);
context.SubmitChanges();
LoadOtherModulesUsed();
}

Flex DataGrid: Adding an item listener to a cell to listen for ITEM_EDIT_BEGIN in another cell of the same row

I'm a little stumped on how I should go about doing something.
I have a FlexDataGrid with a bunch of columns. One of the columns has an ItemRenderer for it. I want all the cells in this column to listen for an event. The event I want the to listen for is when someone begins editing a cell in the same row that the ItemRenderered cell is in.
So I have this code in my ItemRenderer which gets applied to each cell in the column:
this.addEventListener(FlexDataGridEvent.ITEM_EDIT_BEGINNING, showPopUp);
When showPopUp is called, a button will appear under this ItemRenderer.
The problem is, I don't know how I can make that eventListener work. How can I make this item renderer know when an ITEM_EDIT_BEGINNING event is happening in a different cell?
I'm completely stumped.
Thanks!
Your item renderer is actually a different component, so if you are dispatching them with the this keyword, they are not being seen by the other cells / item renderers.
Probably a bit more involved than you were expecting, but this is how you'd accomplish something like that
1) You will want to create a custom event that you can pass the row number with. Something like this will work:
package
{
import flash.events.Event;
public class EditRowEvent extends Event
{
public function EditRowEvent(type:String, rowEditingIn:Number, bubbles:Boolean=false, cancelable:Boolean=false)
{
rowEditing = rowEditingIn;
super(type, bubbles, cancelable);
}
public var rowEditing : Number;
}
}
2) you need to dispatch when editing it to something all the item renders can see. Something like the parent grid:
DataGrid(this.parentDocument).dispatchEvent( new EditRowEvent( 'beginEdit', this_renderers_row ) );
3) You need to listen for this event on the parent as well (do this when the item renderer is initialized) :
protected function onCreationComplete(event:FlexEvent=null):void
{
DataGrid(this.parentDocument).addEventListener( 'beginEdit' , handleEditOnRow );
}
4) handle what you want to happen when that editing starts on the row of that particular item renderer
public function handleEditOnRow ( event : EditRowEvent ) : void {
if( this_renderers_row == event.rowEditing ){
// code to execute when someone starts editing this row!
}
}

'Databinding complete' event for Silverlight 4.0 DataGrid?

I have a DataGrid that I have bound to a property:
<cd:DataGrid
Name="myDataGrid"
ItemsSource="{Binding Mode=OneWay,Path=Thingies}"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
...
When the Thingies property changes, once all rows in the DataGrid have been populated with the new contents of Thingies, I want the DataGrid to scroll to the bottom row.
In WinForms, I would have done this by subscribing to the DataBindingComplete event. MSDN Forums contains several suggestions on how to do this with Silverlight 4.0 but they range from completely evil to just plain fugly:
start a 100ms timer on load, and scroll when it elapses
count rows as they're added, and scroll to the bottom when the number of added rows equals the number of entities in the data source
Is there an idiomatic, elegant way of doing what I want in Silverlight 4.0?
I stumbled upon this while searching for a resolution to the same problem. I was finding that when I attempted to scroll the selected item into view after filter and sort changes that I frequently received a run time error (index out of bounds). I knew instinctively that this was because the grid was not populated at that particular moment.
Aaron's suggestion worked for me. When the grid is defined, I add an event listener:
_TheGrid.LayoutUpdated += (sender, args) => TheGrid.ScrollIntoView(TheGrid.SelectedItem, TheGrid.CurrentColumn);
This solved my problem, and seems to silently exit when the parameters are null, too.
Why not derive from DataGrid and simply create your own ItemsSourceChanged event?
public class DataGridExtended : DataGrid
{
public delegate void ItemsSourceChangedHandler(object sender, EventArgs e);
public event ItemsSourceChangedHandler ItemSourceChanged;
public new System.Collections.IEnumerable ItemsSource
{
get { return base.ItemsSource; }
set
{
base.ItemsSource = value;
EventArgs e = new EventArgs();
OnItemsSourceChanged(e);
}
}
protected virtual void OnItemsSourceChanged(EventArgs e)
{
if (ItemSourceChanged != null)
ItemSourceChanged(this, e);
}
}
Use the ScrollIntoView method for achieving this.
myDataGrid.ItemSource = Thingies;
myDataGrid.UpdateLayout();
myDataGrid.ScrollIntoView(MyObservableCollection[MyObservableCollection.Count - 1], myDataGrid.Columns[1]);
You don't need to have any special event for this.
I think the nice way to do it, in xaml, is to have the binding NotifyOnTargetUpdated=true, and then you can hook the TargetUpdated to any event of your choice.
<ThisControl BindedProperty="{Binding xxx, NotifyOnTargetUpdated=true}"
TargetUpdated="BindingEndedHandler">

Resources