I am trying to use the drag re-ordering on my radgrid. The code I have works well for me (it fires on the RowDrop event) but my client cant get it to work and I have troubleshooted it down to show that when he does the drop, the DestDataItem property of the args is null, so the drop logic never triggers?!? here is my code:
protected void questionGrid_RowDrop(object sender, GridDragDropEventArgs e)
{
if (e.DestDataItem != null)
{
int tempId = int.Parse(editingId.Value);
theTemplate = ent.SurveyTemplates.Where(i => i.Id == tempId).FirstOrDefault();
int id = int.Parse(e.DraggedItems[0]["Id"].Text);
SurveyQuestion draggedQuestion = ent.SurveyQuestions.Where(i => i.Id == id).FirstOrDefault();
List<SurveyQuestion> tempArray = theTemplate.Questions.OrderBy(i => i.Rank).ToList();
tempArray.Remove(draggedQuestion);
tempArray.Insert(e.DestDataItem.ItemIndex, draggedQuestion);
int j = 0;
foreach (SurveyQuestion sq in tempArray)
{
sq.Rank = j;
j++;
}
ent.SaveChanges();
questionGrid.Rebind();
}
else
{
Exceptions.LogException(new Exception("NULL DEST"));
}
}
It just references the dragged item and pulls it from the list of items and re-inserts it at the new index, then it updates the rank property of each item to its new index and saves.
Why would this work for me and not for him? Could this server side code be bothered by browser differences?
As mentioned in this thread, if the item isn't dropped on an actual data row in the grid, the DestDataItem will be null.
You can prevent your RowDrop event from firing, if the target isn't a data row, by handling the OnRowDropping event on the client side and ignoring the things you don't want:
function gridRowDropping(sender, args)
{
if (!args.get_targetGridDataItem())
args.set_cancel(true);
}
Related
I'm trying to come up with a System.Windows.Interactivity.Behaviour that, when applied to a WPF DataGrid, adds a context menu (or items to an existing context menu) that allow users to show or hide columns.
I came up with a solution that almost works very well.
Everything works just as expected - until you hide and then re-show a column. Once becoming visible again, the contextmenu just seems to disappear, right-clicking on the column doesn't do anything anymore.
Code is below, verbalyl what I'm doing:
On attaching the behavior, I start listening to the DataGrid "Loaded" event
In the Loaded event, I find all DataGridColumnHeader descendants of the DataGrid
For each of these, I generate an individual context menu and attach it to the DataGridColumnHeader
For each context menu, I generate one menu item per column, and assign a command to it that, upon execution, sets the DataGridColumn's visibility to Visible or Hidden
I've stripped down the code to a minimum example for the simplest case: To test this, simply apply that behavior to a DataGrid that doesn't have a ContextMenu assigned currently.
public class DgColumnBehavior : Behavior<DataGrid>
{
protected ICommand ToggleColumnVisibilityCmd;
protected DataGrid _AssociatedObject;
protected override void OnAttached()
{
this.ToggleColumnVisibilityCmd = new DelegateCommand<DataGridColumn>(ToggleColumnVisibilityCmdExecute);
this._AssociatedObject = (DataGrid)this.AssociatedObject;
Observable.FromEventPattern(this._AssociatedObject, "Loaded")
.Take(1)
.Subscribe(x => _AssociatedObject_Loaded());
base.OnAttached();
}
void _AssociatedObject_Loaded()
{
var columnHeaders = this._AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(); // see second code piece for the SafeFindDescendants extension method
foreach (var columnHeader in columnHeaders)
{
EnsureSeparateContextMenuFor(columnHeader);
if (columnHeader.ContextMenu.ItemsSource != null)
{
// ContextMenu has an ItemsSource, so need to add items to that -
// ommitted though as irrelevant for example
}
else
{
// No ItemsSource assigned to the Menu, so we can just add directly
foreach (var item in CreateMenuItemsFor(columnHeader))
columnHeader.ContextMenu.Items.Add(item);
}
}
}
/// Ensures that the columnHeader ...
/// A) has a ContextMenu, and
/// B) that is has an individual context menu, i.e. one that isn't shared with any other DataGridColumnHeaders.
///
/// I'm doing that as in practice, I'm adding some further items that are specific to each column, so I can't have a shared context menu
private void EnsureSeparateContextMenuFor(DataGridColumnHeader columnHeader)
{
if (columnHeader.ContextMenu == null)
{
columnHeader.ContextMenu = new ContextMenu();
}
else
{
// clone the existing menu
// ommitted as irrelevant for example
}
}
/// Creates one menu item for each column of the underlying DataGrid to toggle that column's visibility
private IEnumerable<FrameworkElement> CreateMenuItemsFor(DataGridColumnHeader columnHeader)
{
foreach (var column in _AssociatedObject.Columns)
{
var item = new MenuItem();
item.Header = String.Format("Toggle visibility for {0}", column.Header);
item.Command = ToggleColumnVisibilityCmd;
item.CommandParameter = column;
yield return item;
}
}
// Gets executed when the user clicks on one of the ContextMenu items
protected void ToggleColumnVisibilityCmdExecute(DataGridColumn column)
{
bool isVisible = (column.Visibility == Visibility.Visible);
Visibility newVisibility = (isVisible) ? Visibility.Hidden : Visibility.Visible;
column.Visibility = newVisibility;
}
}
The SafeFindDescendants extension method is heavily based on the one from here: DataGridColumnHeader ContextMenu programmatically
public static class Visual_ExtensionMethods
{
public static IEnumerable<T> SafeFindDescendants<T>(this Visual #this, Predicate<T> predicate = null) where T : Visual
{
if (#this != null)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(#this);
for (int i = 0; i < childrenCount; i++)
{
var currentChild = VisualTreeHelper.GetChild(#this, i);
var typedChild = currentChild as T;
if (typedChild == null)
{
var result = ((Visual)currentChild).SafeFindDescendants<T>(predicate);
foreach (var r in result)
yield return r;
}
else
{
if (predicate == null || predicate(typedChild))
{
yield return typedChild;
}
}
}
}
}
}
I can't figure out what's going on. Why does the context menu seem to be removed after hiding/re-showing a column?!
Appreciate any ideas! Thanks.
I've come up with a quick and dirty Fix. It works, but it isn't pretty. Maybe someone can think of a better solution.
Essentially, each time the visibility of a DataGridColumn item changes to hidden/collapsed, I retrieve its DataGridColumnHeader and store the associated context menu in a cache.
And each time the visibility changes back to visible, I'm listening to the next DataGrid LayoutUpdated event (to ensure the visual tree has been built), retrieve the DataGridColumnHeader again - which will incoveniently be a different instance than the original one - and set its context menu to the cached one.
protected IDictionary<DataGridColumn, ContextMenu> _CachedContextMenues = new Dictionary<DataGridColumn, ContextMenu>();
protected void ToggleColumnVisibilityCmdExecute(DataGridColumn column)
{
bool isVisible = (column.Visibility == Visibility.Visible);
Visibility newVisibility = (isVisible) ? Visibility.Hidden : Visibility.Visible;
if(newVisibility != Visibility.Visible)
{
// We're hiding the column, so we'll cache its context menu so for re-use once the column
// becomes visible again
var contextMenu = _AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(z => z.Column == column).Single().ContextMenu;
_CachedContextMenues.Add(column, contextMenu);
}
if(newVisibility == Visibility.Visible)
{
// The column just turned visible again, so we set its context menu to the
// previously cached one
Observable
.FromEventPattern(_AssociatedObject, "LayoutUpdated")
.Take(1)
.Select(x => _AssociatedObject.SafeFindDescendants<DataGridColumnHeader>(z => z.Column == column).Single())
.Subscribe(x =>
{
var c = x.Column;
var cachedMenu = _CachedContextMenues[c];
_CachedContextMenues.Remove(c);
x.ContextMenu = cachedMenu;
});
}
column.Visibility = newVisibility;
}
Have you already found a better solution?
I have a new DataGrid class, so "this" is the actual Instance of a DataGrid!
This is my solution (I'm also listen on the LayoutUpdated event):
this.LayoutUpdated += (sender, args) =>
{
foreach (DataGridColumnHeader columnHeader in GetVisualChildCollection<DataGridColumnHeader>(this))
{
if(columnHeader.ContextMenu == null)
ContextMenuService.SetContextMenu(columnHeader, _ContextMenu);
}
};
public static List<T> GetVisualChildCollection<T>(object parent) where T : Visual
{
List<T> visualCollection = new List<T>();
GetVisualChildCollection(parent as DependencyObject, visualCollection);
return visualCollection;
}
private static void GetVisualChildCollection<T>(DependencyObject parent, List<T> visualCollection) where T : Visual
{
int count = VisualTreeHelper.GetChildrenCount(parent);
for (int i = 0; i < count; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(parent, i);
if (child is T)
{
visualCollection.Add(child as T);
}
else if (child != null)
{
GetVisualChildCollection(child, visualCollection);
}
}
}
I have dynamically added controls to my flowLayoutPanel and I'm offering the user to choose which pair of label and richTextBox does he wants to delete (label text is simply 1.,2.,3.,...) and tags are just numbers(1,2,3,...). This is how I did deleting the controls:
pairToDelete = Convert.ToInt32(textBox.Text);
foreach (Control ctrl in flowLayoutPanel1.Controls.OfType<Label>())
{
if (ctrl.Tag.ToString() == pairToDelete.ToString())
{
Controls.Remove(ctrl);
ctrl.Dispose();
}
}
foreach (Control ctrl in flowLayoutPanel1.Controls.OfType<RichTextBox>())
{
if (ctrl.Tag.ToString() == pairToDelete.ToString())
{
Controls.Remove(ctrl);
ctrl.Dispose();
}
}
Now, what I want is to change the tags of next pairs of controls. For example, if the user wants to delete 2nd pair of label and RTBox then I want to change tags of label3 and RTBox3 from 3 to 2, tags of label4 and RTBox4 from 4 to 3 etc. How can I do this?
I modified a little bit the mechanism to find a control to remove. After that I removing the control the way you remove it. After that I'm lowering the tag number of any control that has the tag higher then the removed control. Assumption is that the tags are numbers. I leave for you to do appropriate checks.
public void Delete()
{
var pairToDelete = Convert.ToInt32(textBox1.Text);
// Find what to remove.
var lblToDelete = this.Controls.OfType<Label>()
.FirstOrDefault(l => l.Tag.ToString() == pairToDelete.ToString());
var txtToDelete = this.Controls.OfType<RichTextBox>()
.FirstOrDefault(c => c.Tag.ToString() == pairToDelete.ToString());
// Can be removed?
if (lblToDelete != null)
{
// Remove.
this.Controls.Remove(lblToDelete);
lblToDelete.Dispose();
// Lower tag number for labels with tag higher then the removed one.
foreach (var c in this.Controls.OfType<Label>()
.Where(l => Convert.ToInt32(l.Tag) > pairToDelete))
{
var newTag = Convert.ToInt32(c.Tag) - 1;
c.Tag = newTag;
}
}
// Can be removed?
if (txtToDelete != null)
{
// Remove.
this.Controls.Remove(txtToDelete);
txtToDelete.Dispose();
// Lower tag number for rich textvbox with tag higher then the removed one.
foreach (var c in this.Controls.OfType<RichTextBox>()
.Where(r => Convert.ToInt32(r.Tag) > pairToDelete))
{
var newTag = Convert.ToInt32(c.Tag) - 1;
c.Tag = newTag;
}
}
}
In these situations where different controls are related, I prefer to rely on an additional storage (on top of the Controls collection) to ease all the actions among controls. For example, a class like the following one:
public class LabelRTB
{
public Label label;
public RichTextBox rtb;
public LabelRTB(Label label_arg, RichTextBox rtb_arg)
{
label = label_arg;
rtb = rtb_arg;
}
}
It can be populated on form load:
List<LabelRTB> allLabelRTB = new List<LabelRTB>();
allLabelRTB.Add(new LabelRTB(label1, rtb1));
allLabelRTB.Add(new LabelRTB(label2, rtb2));
this.Tag = allLabelRTB;
The requested deletion is now straightforward and the indices are updated automatically. For example:
private void removeItem(int index)
{
LabelRTB curItem = ((List<LabelRTB>)this.Tag)[index];
Controls.Remove(curItem.label);
Controls.Remove(curItem.rtb);
((List<LabelRTB>)this.Tag).Remove(curItem);
}
As far as the number of controls is quite limited and the complexity of the associated actions might be reduced notably, this a-priori-less-efficient approach (it stores the same information twice) might even make the application more efficient than in case of exclusively relying on Controls.
This is also one of the solutions:
foreach (Control ctrl in flowLayoutPanel1.Controls.OfType<Label>())
{
if (ctrl.Tag.ToString() == pairToDelete.ToString())
{
Controls.Remove(ctrl);
ctrl.Dispose();
}
if (Convert.ToInt32(ctrl.Tag) > pairToDelete)
{
int decrease = Convert.ToInt32(ctrl.Tag) - 1;
ctrl.Tag = decrease;
ctrl.Text = decrease + ".";
}
}
foreach (Control ctrl in flowLayoutPanel1.Controls.OfType<RichTextBox>())
{
if (ctrl.Tag.ToString() == pairToDelete.ToString())
{
Controls.Remove(ctrl);
ctrl.Dispose();
}
if (Convert.ToInt32(ctrl.Tag) > pairToDelete)
{
int decrease = Convert.ToInt32(ctrl.Tag) - 1;
ctrl.Tag = decrease;
}
}
I am trying to create ASP.NET server control (pure code, without ascx template - because control must be completly contained in .dll and it must not rely on external .ascx files), and I have a problem with dynamically adding items to repeater.
I want to add item to repeater in reaction to SelectedIndexChanged event, but when i do second DataBind() in that event, i lose data from ViewModel (for example, textboxes contains default data instead of text entered by user).
Simplified version of my code (in large portion borrowed from MS composite control example - http://msdn.microsoft.com/en-us/library/3257x3ea%28v=vs.100%29.aspx):
[ToolboxData("<{0}:FilterControl runat=server />")]
public class FilterControl : CompositeControl, IPostBackDataHandler
{
private List<FilteringProperty> elements = new List<FilteringProperty>();
private DropDownList filteringElementsDropDownList;
private Repeater usedFiltersRepeater;
[Bindable(true), DefaultValue(null), Description("Active filters")]
public List<FilteringProperty> UsedElements
{
get
{
EnsureChildControls();
if (ViewState["UsedElements"] == null)
{
ViewState["UsedElements"] = new List<FilteringProperty>();
}
return (List<FilteringProperty>)ViewState["UsedElements"];
}
set
{
EnsureChildControls();
ViewState["UsedElements"] = value;
}
}
protected override void RecreateChildControls()
{
EnsureChildControls();
}
protected override void CreateChildControls()
{
Controls.Clear();
filteringElementsDropDownList = new DropDownList { AutoPostBack = true };
usedFiltersRepeater = new Repeater();
foreach (var element in elements)
{
filteringElementsDropDownList.Items.Add(new ListItem(element.DisplayName));
}
filteringElementsDropDownList.SelectedIndexChanged += (sender, e) =>
{
string selectedText = filteringElementsDropDownList.SelectedValue;
FilteringProperty condition = elements.First(x => x.DisplayName == selectedText);
var toRemove = filteringElementsDropDownList.Items.Cast<ListItem>().FirstOrDefault(x => x.Text == condition.DisplayName);
if (toRemove != null)
{
filteringElementsDropDownList.Items.Remove(toRemove);
}
UsedElements.Add(condition);
// ======> A <========
};
usedFiltersRepeater.ItemDataBound += (sender, args) =>
{
FilteringProperty dataItem = (FilteringProperty)args.Item.DataItem;
Control template = args.Item.Controls[0];
TextBox control = (TextBox)template.FindControl("conditionControl");
control.Text = dataItem.DisplayName;
// ======> C <========
};
usedFiltersRepeater.ItemTemplate = // item template
usedFiltersRepeater.DataSource = UsedElements;
usedFiltersRepeater.DataBind();
// ======> B <========
Controls.Add(filteringElementsDropDownList);
Controls.Add(usedFiltersRepeater);
}
}
I marked important portions of code with (A), (B) and (C)
The problem is, (A) is executed after DataBinding (B and C), so changes in UsedElements are not visible until next postback.
It is possible to add usedFiltersRepeater.DataBind(); after (A), but than all controls are recreated without data from viewstate (i.e empty)
Is there a way to dynamically change repeater after databinding, such that data of contained controls is preserved?
Tl;dr - i have a DropDownList and I want to add editable items to Repeater on SelectedIndexChanged (without losing viewstate).
I finally solved my problem.
My solution is rather dirty, but it seems to work fine.
Instead of simple databinding:
I get state from all controls in repeater and save it in temporary variable (state for each control includes everything, such as selected index for dropdownlists) using my function GetState()
modify this state in any way i want
restore full state using my function SetState()
For example:
FilterState state = GetState();
state.Conditions.Add(new ConditionState { Item = condition });
SetState(state);
I have bookmarks list, when I tap on a bookmark, another page is loaded with a listA consists of several items.
Now suppose, I tap on a bookmark, which points to the item index 100 of the listA... the other listA opens, I manage to set the SelectedIndex of the listA to 100, which is somewhere down the list is not visible.
The problem is that, the SelectedIndex is set to 100, but the list still shows the top most item, on the top.
How can I set the item number 100 on the top, when it loads the contents?
Works perfectly with ScrollViewer.ScrollToVerticalOffset Method
Step I. Call Loaded event of the listA
<ListBox Name="ListA" Loaded="HookScrollViewer">
Step II. Define the "HookScrollViewer" method
private void HookScrollViewer(object sender, RoutedEventArgs e)
{
var element = (FrameworkElement)sender;
var scrollViewer = FindChildOfType<ScrollViewer>(element);
if (scrollViewer == null)
return;
scrollViewer.ScrollToVerticalOffset(lstA.SelectedIndex);
}
Step III. Define the "FindChildOfType" method
public static T FindChildOfType<T>(DependencyObject root) where T : class
{
var queue = new Queue<DependencyObject>();
queue.Enqueue(root);
while (queue.Count > 0)
{
var current = queue.Dequeue();
for (int i = VisualTreeHelper.GetChildrenCount(current) - 1; 0 <= i; i--)
{
var child = VisualTreeHelper.GetChild(current, i);
var typedChild = child as T;
if (typedChild != null)
{
return typedChild;
}
queue.Enqueue(child);
}
}
return null;
}
And it works with "ListA" (Replace ListA by the name of your ListBox)
Ref: ListBox offset in WP7
ListBox is really pain in the ass (especially with complex datatemplates and virtualizing). You should use this workaround:
listbox.SelectedIndex = 1000; //I mean index of the last item
listbox.UpdateLayout();
listbox.SelectedIndex = 100;
listbox.UpdateLayout();
Hope this helps
Try this:
listBox2.TopIndex = listBox2.SelectedIndex;
I'm not sure if I understand your question correctly. But If you just want to set an item on top of the listbox items then I would do..
listbox.Items.Insert(0, "something);
or
Use linq to order them
listbox.Items.OrderBy("something");
I have a custom servercontrol that inherits from CompositeDataBoundControl. I have three templates: one header template, one footer template and one item template. The item template can contain a checkbox that I use to decide if I should delete the item.
In the footer and/or header templates I have a button with a CommandName of "DeleteItem". When that button is clicked, I handle the event in OnBubbleEvent:
if (cea.CommandName == "DeleteItem") {
//loop through the item list and get the selected rows
List<int> itemsToDelete = new List<int>();
foreach(Control c in this.Controls){
if (c is ItemData) {
ItemData oid = (ItemData)c;
CheckBox chkSel = (CheckBox)oid.FindControl("chkSelected");
if (chkSel.Checked) {
itemsToDelete.Add(oid.Item.Id);
}
}
}
foreach (int id in itemsToDelete) {
DeleteItem(id);
}
}
}
The problem is that Item is null since the CreateChildControls method already has been run as asp.net needs to recreate the control hierarchy before the event fire. It uses the DummyDataSource and a list of null objects to recreate the control hierarchy:
IEnumerator e = dataSource.GetEnumerator();
if (e != null) {
while (e.MoveNext()) {
ItemData container = new ItemData (e.Current as OrderItem);
ITemplate itemTemplate = this.ItemTemplate;
if (itemTemplate == null) {
itemTemplate = new DefaultItemTemplate();
}
itemTemplate.InstantiateIn(container);
Controls.Add(container);
if (dataBinding) {
container.DataBind();
}
counter++;
}
}
The problem is this line: ItemData container = new ItemData (e.Current as OrderItem); When the control hierarchy is rebuilt before the event is fired, the e.Current is null, so when I try to find out which item was marked for deletion, I get 0 since the original value has been overwritten.
Any suggestions on how to fix this?
I've finally found a solution that works. The problem is that the bound data is only connected to the control when being bound and directly after(normally accessed in a ItemDataBound event).
So to solve it I had to add a hidden literal containing the data item id to the container control. In the OnBubbleEvent I find the hidden literal and get the id:
ItemData oid = (ItemData)c;
CheckBox chkSel = (CheckBox)oid.FindControl("chkSelected");
if(chkSel != null) {
if(chkSel.Checked) {
Literal litId = (Literal)oid.FindControl("litId");
itemsToDelete.Add(Utils.GetIntegerOnly(litId.Text));
}
}