Dynamic Gridview - item template control event handling - c#

I have created a custom gridview control that inherits the asp.net gridview. I am required to use item templates in this gridview. I create a method in my custom gridview that generates the item template.
public void addTemplateField(Control headerTemplateControl, Control itemTemplateControl, EventHandler bindHandler, EventHandler initHandler, string headerText, string sortExpression, bool isVisible, int? heightPx, int? widthPercent)
{
TemplateField tField = new TemplateField();
if (headerTemplateControl != null)
tField.HeaderTemplate = new GridViewTemplate(ListItemType.Header, headerTemplateControl);
if (bindHandler != null && initHandler != null)
tField.ItemTemplate = new GridViewTemplate(ListItemType.Item, itemTemplateControl, bindHandler, initHandler);
else if (bindHandler != null)
tField.ItemTemplate = new GridViewTemplate(ListItemType.Item, itemTemplateControl, bindHandler, false);
else if (initHandler != null)
tField.ItemTemplate = new GridViewTemplate(ListItemType.Item, itemTemplateControl, initHandler, true);
else
tField.ItemTemplate = new GridViewTemplate(ListItemType.Item, itemTemplateControl);
if (sortExpression != null)
tField.SortExpression = sortExpression;
tField.Visible = isVisible;
if (headerText != null)
tField.HeaderText = headerText;
if (heightPx.HasValue)
tField.HeaderStyle.Height = new Unit(heightPx.Value, UnitType.Pixel);
if (widthPercent.HasValue)
tField.HeaderStyle.Height = new Unit(widthPercent.Value, UnitType.Percentage);
addColumnField(tField);
}
And this is how I have implemented ITemplate
public class GridViewTemplate : ITemplate
{
int _controlCount = 0;
ListItemType _templateType;
EventHandler _bindHandler;
EventHandler _initHandler;
Control _control;
public GridViewTemplate(ListItemType type, Control control)
{
this._templateType = type;
this._control = control;
}
public GridViewTemplate(ListItemType type, Control control, EventHandler Handler, bool isInitHandler)
{
this._templateType = type;
this._control = control;
if (isInitHandler)
this._initHandler = Handler;
else
this._bindHandler = Handler;
}
public GridViewTemplate(ListItemType type, Control control, EventHandler bindHandler, EventHandler initHandler)
{
this._templateType = type;
this._control = control;
this._bindHandler = bindHandler;
this._initHandler = initHandler;
}
public Control Copy(Control ctrlSource)
{
Type _type = ctrlSource.GetType();
Control ctrlDest = (Control)Activator.CreateInstance(_type);
foreach (PropertyInfo prop in _type.GetProperties())
{
if (prop.CanWrite)
{
if (prop.Name == "ID")
{
ctrlDest.ID = ctrlSource.ID + "_copy_" + _controlCount;
}
else
{
prop.SetValue(ctrlDest, prop.GetValue(ctrlSource, null), null);
}
}
}
_controlCount++;
return ctrlDest;
}
public void InstantiateIn(Control container)
{
switch (_templateType)
{
case ListItemType.Header:
container.Controls.Add(_control);
break;
case ListItemType.Item:
Control temp = Copy(_control);
if(_bindHandler != null)
temp.DataBinding += _bindHandler;
if (_initHandler != null)
temp.Init += _initHandler;
container.Controls.Add(temp);
break;
}
}
}
In the page that needs say Default.aspx.cs, I create this gridview onPreInit and attach its event handlers onInit.
I add a checkbox to the grid by calling the addTemplateField().
cbl = new CheckBox();
cbl.AutoPostBack = true;
init = new EventHandler(cbl_Init);
grd.addTemplateField(null, cbl, null, init, "SERVER", null, true, 20, 20);
void cbl_Init(object sender, EventArgs e)
{
CheckBox c = (CheckBox)sender;
c.CheckedChanged +=new EventHandler(cbl_CheckedChanged);
}
void cbl_CheckedChanged(object sender, EventArgs e)
{
// Modify datasource
// databind();
// if i remove this databind, checkchanged is handled every time. If i keep the databind, event is handled only alternate times.
}
The issue is the checkbox checkchanged event is fired for alternate times. Every other time, the page post backs but the checkchanged event is not handled. I am lost in finding the cause, let alone the solution.!?!?!

I found the root cause of the problem. It was in the Copy method of the gridviewtemplate class. The problem being for each postback, the controls generated were being done in a unique id. So on postback the event triggered by the control, had changed its id, so no event was triggered.
To be more crystal...
Page loads initially with controls having a unique id,
Click on the control to trigger an event
The page post backs with the controls being generated with the same id.
Click on the control to trigger the event.
The page posts back, but the controls are generated with a different it that does not match the event source of step 4.
Solution was to remove the control count variable.

Related

C# Dynamic Panel SubPanel-Event Loop bug

I am currently making an UI using a flow layout panel in visual studio 19.
If i press a button it clones a panel using
public static T Clone<T>(this T controlToClone) where T : Control
{
PropertyInfo[] controlProperties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
T instance = Activator.CreateInstance<T>();
foreach (PropertyInfo propInfo in controlProperties)
{
if (propInfo.CanWrite)
{
if (propInfo.Name != "WindowTarget")
propInfo.SetValue(instance, propInfo.GetValue(controlToClone, null), null);
}
}
return instance;
}
In the same press event it fires AddNewPanel("name");
private void AddPanel(string name)
{
var label = new Label();
label.AutoSize = false;
label.TextAlign = ContentAlignment.MiddleCenter;
label.Dock = DockStyle.Fill;
label.Text = name;
label.MouseEnter += labelEnter;
label.MouseLeave += labelLeave;
var panel = panel1.Clone();
var button = button1.Clone();
button.Name = "button" + new Random().Next(1, 100);
panel.Controls.Add(button);
panel.Controls.Add(label);
flowLayoutPanel1.Controls.Add(panel);
}
the event labelEnter triggers ShowButtons()
private void showButtons()
{
foreach (Control item in flowLayoutPanel1.Controls)
{
var button = item.Controls[1];
button.Visible = true;
}
}
and the mouseLeave event does the same except turning Visible to false.
Now I have experienced, dynamically adding Controls to a panel which then gets added to a flowlayout panel causes some Issues. For example, the labelMouseEnter/Leave event gets looped when moving the mouse over the button. Anyone experienced this before or similiar posts regarding this? It doesn't happen when the button is initially Visible, meaning Visible is turned to true in designer View. It clones it with the visible attribute.
The Solution is to add a check in the MouseLeave event as following:
private void control_MouseLeave(object sender, EventArgs e)
{
var control = sender as Control;
if (control.ClientRectangle.Contains(control.PointToClient(Cursor.Position))) return;
//rest
ShowButtons();
}

How to get the underlying (TextBox) Control of a DataGridTemplateColumn

I use this code to add a TextBox to a DataGrid cell: (no, I can't use XAML here)
Binding binding = new Binding("Fld_Company");
binding.Mode = BindingMode.OneWay;
FrameworkElementFactory frameworkElementFactory = new FrameworkElementFactory(typeof(TextBox));
DataTemplate dataTemplate = new DataTemplate();
dataTemplate.VisualTree = frameworkElementFactory;
frameworkElementFactory.SetBinding(TextBox.TextProperty, binding);
DataGridTemplateColumn dataGridTemplateColumn = new DataGridTemplateColumn();
dataGridTemplateColumn.IsReadOnly = true;
dataGridTemplateColumn.Header = "Company";
dataGridTemplateColumn.CellTemplate = dataTemplate;
this.dataGrid.Columns.Add(dataGridTemplateColumn);
I there a way to get the underlying TextBox control without XAML?
What I tried:
VisualTreeHelper, but the GetChildrenCount() is always 0
FindName, but I haven't found a proper FrameworkElement
After explored the DataGrid for a while I see that my question does not make any sense. My code above prepares just the DataGrid but does not fill any data. Until that no rows are generated and therefore no underlying TextBox controls can be found.
When the DataGrid gets finally filled with data, the best way to get the underlying controls seems to be catching the LoadinRow event. But when this event fires, the loading of the row is not finished. There needs to be temporarily assigned a second event which fires when the row is finally loaded.
{
DataGrid.LoadingRow += DataGrid_LoadingRow;
}
private void DataGrid_LoadingRow(object sender, DataGridRowEventArgs e)
{
// The visual tree is not built until the row is "loaded". This event fires when this happend:
e.Row.Loaded += DataGrid_Row_Loaded;
}
private void DataGrid_Row_Loaded(object sender, RoutedEventArgs e)
{
DataGridRow dataGridRow = (DataGridRow)sender;
// important: Remove the event again
dataGridRow.Loaded -= DataGrid_Row_Loaded;
NestedGridFieldProperty ngrProp = (NestedGridFieldProperty)dataGridRow.Item;
// Get the "presenter", which contains the cells
DataGridCellsPresenter presenter = coNeboTools.ConeboMisc.GetVisualChild<DataGridCellsPresenter>(dataGridRow);
// Get the cells in the presenter
var cells = GetVisualChildren<DataGridCell>(presenter);
// Get the underlying TextBox in column 0
TextBox underlyingTextBox = (TextBox)cells.ElementAt(0).Content;
// the Item property of the row contains the row data
var myData = dataGridRow.Item;
// do what ever is needed with the TextBlock
underlyingTextBox.Foreground = Brushes.Red;
}
// Static helper method to handle the visual tree
public static IEnumerable<T> GetVisualChildren<T>(DependencyObject dependencyObject)
where T : DependencyObject
{
if (dependencyObject != null)
{
int childrenCount = VisualTreeHelper.GetChildrenCount(dependencyObject);
for (int i = 0; i < childrenCount; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(dependencyObject, i);
if (child != null && child is T)
{
yield return (T)child;
}
foreach (T childOfChild in GetVisualChildren<T>(child))
{
yield return childOfChild;
}
}
}
}
// Static helper method to handle the visual tree
public static childItem GetVisualChild<childItem>(DependencyObject obj)
where childItem : DependencyObject
{
foreach (childItem child in GetVisualChildren<childItem>(obj))
{
return child;
}
return null;
}

How to Set Index of ListPicker without Executing ValueChanged Event

This problem has been driving me absolutely crazy. I have a ListPicker that is populated dynamically with several items. I have placed declared my SelectionChanged event handler in the Loaded event of my page. When the user clicks a certain item on the page, the ListPicker visibility will toggle from Collpased to Visible, and I set the values of the ListPicker. The issue is, the index of the ListPicker will be based upon the users settings, so out of three items the current index may be 1, not 0 as is the default. I need to show 1 as the current item in the ListPicker without the SelectionChanged event firing (which performs operations based on the current index). Then and only then when the user changes the selected item himself or herself do I need the SelectionChanged event to fire.
The main reason for this is not only does the user need to see his or her current setting in the ListPicker when it is displayed, but on the SelectionChanged event operations occur which overwrite what currently exists which is extremely confusing and not supposed to occur unless the user specifies.
What I currently have is as follows
XAML
<toolkit:ListPicker x:Name="lp" Visibility="Collapsed" Margin="12" Width="300"/>
XAML.CS
private void Page_Loaded(object sender, RoutedEventArgs e)
{
lp.SelectionChanged += lp_SelectionChanged;
}
void EditableEllipse_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
if (sender != null)
{
DependencyObject tappedElement = e.OriginalSource as UIElement;
// find parent UI element of type PhotoThumbnail
//PhotoThumbnail i = this.FindParentOfType<PhotoThumbnail>(tappedElement);
i = this.FindParentOfType<PhotoThumbnail>(tappedElement);
if (i != null)
{
BuildControl(i);
}
}
}
private void BuildControl(PhotoThumbnail pp)
{
switch(pp.NName)
{
case "flip":
List<ListPickerItem> l = new List<ListPickerItem>();
l.Add(new ListPickerItem { Name = "Flip_Vertical", Content = AppResources.App_Flip_Vertical });
l.Add(new ListPickerItem { Name = "Flip_Horizontal", Content = AppResources.App_Flip_Horizontal });
l.Add(new ListPickerItem { Name = "Flip_Both", Content = AppResources.App_Flip_Both });
lp.ItemsSource = l; //Code execution jumps from here to ValueChanged event immediately
lp.Visibility = Visibility.Visible;
lp.SelectedIndex = Settings.Flip.Value - 1;
break;
..
}
}
private async void lp_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (lp.SelectedIndex != -1) //always defaults to = 0
{
var item = (sender as ListPicker).SelectedItem;
string name = ((ListPickerItem)item).Name;
if (name != null)
{
switch (name)
{
case "Flip_Vertical":
Settings.Flip.Value = 1;
..perform process based on current Setting.Flip.Value.. break;
case "Flip_Horizontal":
Settings.Flip.Value = 2;
..perform process based on current Setting.Flip.Value..
break;
case "Flip_Both":
Settings.Flip.Value = 3;
..perform process based on current Setting.Flip.Value..
break;
...
}
}
}
Try using an order of operation technique (by unhooking and rehooking the event)
private void BuildControl(PhotoThumbnail pp)
{
switch(pp.NName)
{
case "flip":
// unhook event
lp.SelectionChanged -= lp_SelectionChanged;
List<ListPickerItem> l = new List<ListPickerItem>();
l.Add(new ListPickerItem { Name = "Flip_Vertical", Content = AppResources.App_Flip_Vertical });
l.Add(new ListPickerItem { Name = "Flip_Horizontal", Content = AppResources.App_Flip_Horizontal });
l.Add(new ListPickerItem { Name = "Flip_Both", Content = AppResources.App_Flip_Both });
lp.ItemsSource = l; //Code execution jumps from here to ValueChanged event immediately
lp.Visibility = Visibility.Visible;
lp.SelectedIndex = Settings.Flip.Value - 1;
// after setting the index, rehook the event
lp.SelectionChanged += lp_SelectionChanged;
break;
..
}
}

How to find a programatically created button by text?

So my program is generating a bunch of buttons like so:
foreach (var subdir in dir.GetDirectories()) {
var path = subdir.Name;
var button = new Button {
Text = getFlavor(path) + "\t(" + path + ")",
Width = Width,
Height = 35,
Top = y
};
button.Click += buttonClick;
Controls.Add(button);
if (button.Text.Contains("Kittens")
i++;
}
I want to try something like this
if (i == 1) {
[Button.ThatContains("Kitten)].Click;
}
"ThatContains" is not a real method. How do I get references to buttons I've created programmatically ?
You could use OfType<Button> to find all buttons in the container control where you've added them(f.e. a Panel). Then a liitle bit LINQ power gives you the correct button(s):
var kittenButtons = panel.Controls.OfType<Button>()
.Where(btn => btn.Text.Contains("Kittens"));
foreach(Button btn in kittenButtons)
btn.PerformClick();
If you just want to click the first:
Button kittenButton = panel.Controls.OfType<Button>()
.FirstOrDefault(btn => btn.Text.Contains("Kittens"));
if(kittenButton != null)
kittenButton.PerformClick();
For what it's worth, here is also an extension method that returns controls recursively via deferred execution which allows to use only the first found Buttton or consume all down the road:
public static IEnumerable<T> GetChildControlsRecursive<T>(this Control root) where T : Control
{
if (root == null) throw new ArgumentNullException("root");
var stack = new Stack<Control>();
stack.Push(root);
while (stack.Count > 0)
{
Control parent = stack.Pop();
foreach (Control child in parent.Controls)
{
if (child is T)
yield return (T)child;
stack.Push(child);
}
}
yield break;
}
Now you can use similar code as above to get for example the first matching button or all:
var kittenButtons = this.GetChildControlsRecursive<Button>()
.Where(b => b.Text.Contains("Kittens"));
// search just until the first button is found
Button firstKittenButton = kittenButtons.FirstOrDefault();
if(firstKittenButton != null) firstKittenButton.PerformClick;
// loop all
foreach(Button btn in kittenButtons)
btn.PerformClick();
Either create a subclass of Button to store the information you want and instantiate that instead or use the Tag property
public class MyButton : Button
{
public int ButtonID { get; set; }
}
public class MyApplication
{
public void DoSomething()
{
int i; // todo: loop stuff
var button = new MyButton
{
Text = getFlavor(path) + "\t(" + path + ")",
Width = Width,
Height = 35,
Top = y,
ButtonID = i
};
}
}
Or why not cast the sender parameter of the button click event as a Button and check the text?
public class MyApplication
{
public void DoSomething()
{
var b = new Button();
b.Click += b_Click;
}
public void b_Click(object sender, EventArgs e)
{
Button b = (Button)sender;
switch (b.Text) {
case "Kittens":
return;
default:
return;
}
}
}
Something like this
var button = FirstOrDefault(y => y is Button && y.Text.Contains("Kittens"));
if(button != null)
button.PerformClick();
In order to get the references, you may need to what you would do with getting references of any other type - store them somewhere, which does not seem to be the case here at all. Normally, you would register your buttons for interaction from a user by attaching them to a Form. Assuming you're not doing this by the looks of your sample code, I'm going to recommend storing them into a Dictionary<string, Button>.
You could use a dictionary or you could use a simple recursive loop (in case you are sticking the buttons into different containers).
private bool ClickButton(string buttonName, Control control) {
if (control is Button && control.Text.Contains(buttonName) {
((Button)control)PerformClick();
return true;
}
if (control.HasChildren) {
foreach (Control childControl in control.Controls) {
if (ClickButton(buttonName, childControl)) {
return true;
}
}
}
return false;
}
Usage: ClickButton("Kittens", this);
Or you could use a dictionary, as some have suggested.
private Dictionary<string, Button> DynamicButtons = new Dictionary<string, Button>();
private void ClickDictionaryButton(string buttonName) {
var matches = DynamicButtons.Where(x => x.Key.Contains(buttonName));
foreach (var match in matches) {
match.Value.PerformClick();
}
}
Usage: ClickDictionaryButton("Kittens", this);

Control acessing event handler in web form class

I have a Custom WebControl. Inside this control I add a button and I want it to access an EventHandler that is on the WebForm where the control is included. The handler handles with controls from the WebForm, so it has to be there. I could probably manage to take the button out of the control, but it would be better to keep it on the control, for organization sake.
public class LanguageSelection : WebControl
{
private List<Language> _Languages;
private CSSImageButton btnOk = new CSSImageButton();
private CSSImageButton btnClose = new CSSImageButton();
public List<Language> Languages
{
set { _Languages = value; }
get { if (_Languages != null) return _Languages; else; _Languages = LanguageManager.Select(); return _Languages; }
}
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
Control parent;
Control container;
btnClose.CssClass = "sprReprove";
btnClose.DivClass = "float-right";
btnClose.OnClientClick = "$('#languagesOptions').hide('slow')";
btnOk.CssClass = "sprApprove";
btnOk.DivClass = "float-right";
btnOk.Click += new ImageClickEventHandler("btnSave_Click"); // this method here is on the webform where i included the control
// Get a reference to the ScriptManager object for the page
// if one exists.
ScriptManager sm = ScriptManager.GetCurrent(Page);
if (sm == null || !sm.EnablePartialRendering)
{
// If partial rendering is not enabled, set the parent
// and container as a basic control.
container = new Control();
parent = container;
}
else
{
// If partial rendering is enabled, set the parent as
// a new UpdatePanel object and the container to the
// content template of the UpdatePanel object.
UpdatePanel up = new UpdatePanel();
container = up.ContentTemplateContainer;
parent = up;
}
container.Controls.Add(new LiteralControl("<div id=\"languagesOptions\" class=\"divSelectLanguages\">"));
container.Controls.Add(new LiteralControl(" <strong>Salvar conteúdo nestes idiomas?</strong>"));
container.Controls.Add(new LiteralControl("<table class=\"tblSelectLanguages\">"));
int i = 0;
foreach (Language l in Languages)
{
CheckBox cb = new CheckBox();
cb.Enabled = false;
if(i % 2 == 0) container.Controls.Add(new LiteralControl("</tr><tr>"));
container.Controls.Add(new LiteralControl("<td>"));
container.Controls.Add(cb);
container.Controls.Add(new LiteralControl(l.FullName));
container.Controls.Add(new LiteralControl("</td>"));
i++;
}
container.Controls.Add(new LiteralControl("</tr>"));
container.Controls.Add(new LiteralControl("</table>"));
container.Controls.Add(btnOk);
container.Controls.Add(btnClose);
container.Controls.Add(new LiteralControl("</div>"));
Controls.Add(parent);
}}
Having your button handled by an event on the containing webform is not advisable. Ideally, your control should be completely self-contained. Instead, what you can do is have your button click event handled inside your control and then raise another event, which can be handled by the WebForm.
// This event will be handled by the webform
public event EventHandler OkButtonClicked;
protected void btnOk_Click(object sender, EventArgs e)
{
// Raise the okButtonClicked event
if (OkButtonClicked != null)
OkButtonClicked(sender, e);
}
// The btnOk button will be wired to our new event handler
btnOk.Click += new ImageClickEventHandler(btnOk_Click);
On your webform, you can have something like this:
<app:LanguageSelection ID="LanguageSelection1" OnOkButtonClicked="btnSave_Click" runat="server"/>
When the button is clicked inside the webcontrol, it would be handled by the btnOk_Click method inside the webcontrol. This would then raise the OkButtonClicked event which would be handled by the btnSave_Click method in WebForm containing the control.

Categories