Override SharePoint:SaveButton in custom form template - c#

First let me clear the air and post articles which already explain how to override the SaveButton:
How to override or customize the Sharepoint SaveButton?
How to override functionality of the Ribbon Save button
Override SharePoint Save Button to Provide Custom Functionality
I have read those and understood them, I just don't know how to fully implement it in my particular case:
I have a custom rendering template "CustomRender" which includes the "real form". The code for the real form looks something around these lines:
<%# Register TagPrefix="wssuc" TagName="ToolBar"
src="~/_controltemplates/ToolBar.ascx" %>
<%# Control Language="C#" AutoEventWireup="true"
CodeBehind="RealForm.ascx.cs" Inherits="CustomNameSpace.CustomForm" %>
<p>Test</p>
<wssuc:ToolBar runat="server" id="toolbar">
<TemplateButtons>
<SharePoint:SaveButton runat="server" />
</TemplateButtons>
</wssuc:ToolBar>
Now I want to override this save button. The sites above state that I just have to write another control which overrides the button. E.g.:
public class NewSaveButton: SaveButton
{
protected override bool SaveItem()
{
bool success = base.SaveItem();
RedirectUrl = String.Concat(List.ParentWeb.ServerRelativeUrl, "/",
List.Forms[PAGETYPE.PAGE_DISPLAYFORM].Url, #"?ID=",
ListItem.ID, #"&Source=", ListItem.ParentList.DefaultViewUrl);
return success;
}
}
Now I just don't know how to register this template inside my other template. Could I not just override the SaveButton in the Code behind of my template - how would I do that and reference it later on?
Option one: Code-Behind of the form (RealForm.ascx.cs) - can I just put the override method in there? How can I reference the button then in the form (how do I get <NewSaveButton>)?
Option two: Another template just for the button, e.g. SaveButton.ascx" - how do I reference that via <%# Register... %>, i.e. how do I know PublicKeyToken etc. when deployed via a Feature. And same thing here: My goal is to get some kind of "<NewSaveButton>" control for the form.

You're creating a new server control when you do this, so you'll need to register the new control on the page (or in this case, in the template .ascx file).
<%# Register TagPrefix="MyPrefix" Namespace="ControlNamespace" Assembly="MyFullyQualifiedAssembly" %>
In your code file you can to add the ToolboxDataAttribute to the class (this is only necessary if you are dragging&dropping the control from the toolbox in visual studio)
[ToolboxData("<{0}:NewSaveButton runat=\"server\"></{0}:NewSaveButton>")]
public class NewSaveButton : SaveButton {}
Now, you should be able to replace the save button on the form with the following:
<MyPrefix:NewSaveButton runat="server"></MyPrefix:NewSaveButton>
You're basically creating a new server control following the rules of asp.net (no sharepoint specific stuff is happening here).
For more information, take a look at this page: http://msdn.microsoft.com/en-us/library/yhzc935f(v=VS.85).aspx

On your page with SaveButton you could do the following trick (in my case save button is added in DataFormWebPart's XSL markup):
// On your page with SaveButton you could do the following trick
// (in my case save button is added in DataFormWebPart's XSL markup):
SPContext itemContext;
DataFormWebPart dataForm; // from designer's code behind
void Page_Init(object sender, EventArgs e)
{
// NOTE: by some reason ItemContexts of controls in DFWP are differ,
// so only SaveButton's OnSaveHandler is invoked
itemContext = dataForm.Controls.FindControlRecursive<SaveButton>().ItemContext;
}
void Page_Load(object sender, EventArgs e)
{
if (itemContext.FormContext.FormMode == SPControlMode.New ||
itemContext.FormContext.FormMode == SPControlMode.Edit)
{
itemContext.FormContext.OnSaveHandler += OnSaveHandler;
}
}
void OnSaveHandler(object sender, EventArgs eventArgs)
{
// TODO: Add your code before saving the item
SaveButton.SaveItem(saveButton.ItemContext, false, string.Empty);
// TODO: Add your code after saving the item
}
The FindControlRecursive() extension implementation is
public static class ControlExtensions
{
public static TControl FindControlRecursive<TControl>
(
this ControlCollection controls
) where TControl : Control
{
if (controls != null)
{
foreach (Control control in controls)
{
var foundControl = control as TControl
?? control.Controls.FindControlRecursive();
if (foundControl != null)
{
return foundControl;
}
}
}
return null;
}
}

Related

Unable to modify the control collection

I'm trying to add a Link to a webform page from a user control. The CSS file contains the styling used by the control. When I try, I get the following exception:
The control collection cannot be modified during DataBind, Init, Load,
PreRender or Unload phase
So, when can anything be added to the controls collection? Seems to me the exception message suggests that all options are off the table. Here's the code:
public partial class AddNoteDlg : UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
RegisterFiles();
}
private void RegisterFiles()
{
Page.ClientScript.RegisterClientScriptInclude(GetType(), "addNoteDlgComponent-1.0.0.js", ResolveUrl("~/Shared/scripts/js/addNoteDlgComponent-1.0.0.js"));
var css = new HtmlLink();
css.Href = ResolveUrl("~/Shared/css/addNoteDlg-0.0.1.css");
css.Attributes["rel"] = "stylesheet";
css.Attributes["type"] = "text/css";
css.Attributes["media"] = "all";
Page.Controls.Add(css);
}
}
Instead, stuck the link tag in the user control markup, as there is no difference, practically speaking.
(The original motivation for doing it programmatically was so I could add it to the header, with Page.Header.Controls.Add(css), allowing me to keep all CSS links in the header.)

I can't get the custom control to change rendering of a master page control

I have a master page with a placeholder control.
I have a page which inherits from master page.
I then have a custom control which is displayed on the page, this custom control has a cast to the masterpage so I can access the placeholder control to turn visibility on and off. Everything works as expected when I watch it in the debugger, but the placeholder control fails to turn it's visibility off.
I feel this has something to do in the order in which the events are firing. It appears any code in the custom control on the page is firing after the masterpage has already rendered.
Does anybody have any idea how I can affect the way this page is rendered so the custom control can turn the placeholder and on and off?
the code in the control looks like this;
protected override void Render(HtmlTextWriter writer)
{
var master = this.Page.Master as Site;
if (master != null) // cast failed, your master is a different type
{
master.NavBar.Visible = false;
}
// other stuff
}
Include the MasterType tag in your page, so that you dont need to typecase the page.Master, directly you can get the Master instance.
The solution to this problem was the order in which the control, masterpage and page were being fired, it was ignoring the setting on the custom control. The solution is to add this functionality to the custom control on the OnPreRender(EventArgs e) method.
protected override void OnPreRender(EventArgs e)
{
var master = this.Page.Master as Site;
if (master != null) // cast failed, your master is a different type
{
var progressShown = master.FindControl("ProgressShown");
if (progressShown != null)
{
master.NavBar.Attributes.Add("class", "test");
}
}
base.OnPreRender(e);
}

How to check whether postback caused by a Dynamic link button

I have a button control. On click of this button I need to add a Link Button dynamically. The Link Button needs an event handler. Hence the dynamic Link button is first added in the Page_Load and cleared and added again in the button click handler. Please read Dynamic Control’s Event Handler’s Working for understanding the business requirement for this.
I have read On postback, how can I check which control cause postback in Page_Init event for identifying the control that caused the postback (inside Page_Load). But it is not working for my scenario.
What change need to be done to confirm whether the postback was caused by link button (inside Page_Load)?
Note: Refer the following for another scenario where it is inevitable https://codereview.stackexchange.com/questions/20510/custom-paging-in-asp-net-web-application
Note 1: I need to get the postback control ID as the first step inside if (Page.IsPostBack). I need to add the dynamic link buttons control only if it is a postback from the button or the link button. There will be other controls that causes postback. For such postbacks we should not execute this code.
Note 2: I am getting empty string for Request["__EVENTARGUMENT"] in the Page_Load
Related Question: By what event, the dynamic controls will be available in the Page (for using in FindControl). #Tung says - "Your GetPostBackControlId method is properly getting the name of the control that caused the postback, but it is unable to find a control with that id through page.FindControl because the linkbutton has not been created yet, and so page does not know of its existence. "
ASPX
<%# Page Language="C#" AutoEventWireup="true" CodeFile="PostbackTest.aspx.cs" Inherits="PostbackTest"
MasterPageFile="~/TestMasterPage.master" %>
<asp:Content ID="myContent" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
<div id="holder" runat="server">
</div>
<asp:Button ID="Button1" runat="server" Text="Button" OnClick="TestClick" />
</asp:Content>
CODE BEHIND
public partial class PostbackTest : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if(Page.IsPostBack)
{
string IDValue = GetPostBackControlId(this.Page);
int x = 0;
holder.Controls.Clear();
LinkButton lnkDynamic = new LinkButton();
lnkDynamic.Click += new EventHandler(LinkClick);
lnkDynamic.ID = "lnkDynamic123";
lnkDynamic.Text = "lnkDynamic123";
holder.Controls.Add(lnkDynamic);
}
}
protected void TestClick(object sender, EventArgs e)
{
holder.Controls.Clear();
LinkButton lnkDynamic = new LinkButton();
lnkDynamic.Click += new EventHandler(LinkClick);
lnkDynamic.ID = "lnkDynamic123";
lnkDynamic.Text = "lnkDynamic123";
holder.Controls.Add(lnkDynamic);
}
protected void LinkClick(object sender, EventArgs e)
{
}
public static string GetPostBackControlId(Page page)
{
if (!page.IsPostBack)
{
return string.Empty;
}
Control control = null;
// First check the "__EVENTTARGET" for controls with "_doPostBack" function
string controlName = page.Request.Params["__EVENTTARGET"];
if (!String.IsNullOrEmpty(controlName))
{
control = page.FindControl(controlName);
}
else
{
// if __EVENTTARGET is null, the control is a button type
string controlId;
Control foundControl;
foreach (string ctl in page.Request.Form)
{
// Handle ImageButton they having an additional "quasi-property" in their Id which identifies mouse x and y coordinates
if (ctl.EndsWith(".x") || ctl.EndsWith(".y"))
{
controlId = ctl.Substring(0, ctl.Length - 2);
foundControl = page.FindControl(controlId);
}
else
{
foundControl = page.FindControl(ctl);
}
if (!(foundControl is Button || foundControl is ImageButton)) continue;
control = foundControl;
break;
}
}
return control == null ? String.Empty : control.ID;
}
}
REFERENCE
On postback, how can I check which control cause postback in Page_Init event
Dynamic Control’s Event Handler’s Working
Understanding the JavaScript __doPostBack Function
Access JavaScript variables on PostBack using ASP.NET Code
How does ASP.NET know which event to fire during a postback?
how to remove 'name' attribute from server controls?
How to use __doPostBack()
A postback in asp.net is done by the java script function __doPostback(source, parameter)
so in your case it would be
__doPostback("lnkDynamic123","") something like this
So in the code behind do the following
var btnTrigger = Request["__EVENTTARGET"];
if(btnTrigger=="lnkDynamic123")
{
}
--- this would tell that it is your linkbutton that causes the postback
You can move the call to the GetPostBackControlId method after the LinkButton has been added to the page:
protected void Page_Load(object sender, EventArgs e)
{
if (Page.IsPostBack)
{
holder.Controls.Clear();
LinkButton lnkDynamic = new LinkButton();
lnkDynamic.Click += new EventHandler(LinkClick);
lnkDynamic.ID = "lnkDynamic123";
lnkDynamic.Text = "lnkDynamic123";
holder.Controls.Add(lnkDynamic);
string IDValue = GetPostBackControlId(this.Page);
if (IDValue == lnkDynamic.ID)
LinkClick(lnkDynamic, new EventArgs());
}
}
Calling the click event handler here also more closely mimics the standard ASP.NET Page Life Cycle, where Postback event handling occurs after the Load event.
Edit:
If the control ID must be determined before the LinkButtons are created, you can create a naming scheme for the link button IDs, e.g. lnkDynamic_1, lnkDynamic_2 etc.
Request["__EVENTTARGET"] will then contain the auto-generated control ID such as “ctl00$mc$lnkDynamic_1”, which you can use to identify which LinkButton caused the postback.
If You're getting the post back control id correctly but FindControl returns nothing, then it's probably because You're using a master page. Basically, someControl.FindControl(id) searches through controls that are in someControl.NamingContainer naming container. But in Your case, the Button1 control is in the ContentPlaceHolder1, which is a naming container, and not directly in the Page naming container, so You won't find it by invoking Page.FindControl. If You can't predict in which naming container the control You're looking for is going to be (e.g. post back can be caused by two different buttons from two different content placeholders), then You can write an extension that'll search for a control recursively, like so:
public static class Extensions
{
public static Control FindControlRecursively(this Control control, string id)
{
if (control.ID == id)
return control;
Control result = default(Control);
foreach (Control child in control.Controls)
{
result = child.FindControlRecursively(id);
if (result != default(Control)) break;
}
return result;
}
}
Use it with caution though, because this method will return the first control that it finds with the specified id (and You can have multiple controls with the same id - but they should be in different naming containers; naming containers are meant to differentiate between controls with same ids, just as namespaces are meant to differentiate between classes with same names).
Alternatively, You could try to use FindControl(string id, int pathOffset) overload, but I think it's pretty tricky.
Also, check this question out.
First approach (wouldn't recommend but it's more flexible)
One completely different approach - although I don't really feel like I should promote it - is to add a hidden field to the form.
The value of this hidden field might be something like false by default.
In case of clicking one of the dynamic buttons which should cause the dynamic controls to be added again, you can simply change the hidden fields value to true on client side before performing the postback (eventually you want/have to modify the client side onclick handler to make this happen).
Of course it would be possible to store more information in such a field, like the controls id and the argument (but you can get those values as described in the other answers). No naming schema would be required in this case.
This hidden field could be "static". So it would be accessible in code behind all time. Anyhow, you might want to implement something to make sure that nobody is playing around with its values and fakes a callback which looks like it originated from one of these dynamic links.
However, this whole approach just helps you getting the id of the control. Until you create the control again, you won't be able to get the instance through NamingContainer.FindControl (as mentioned in the other answers already ;)). And in case you create it, you don't need to find it anymore.
Second approach (might not be suitable due to its contraints)
If you want to do it the clean way, you need to create your controls OnLoad, no matter if something was clicked or not. Additionally, the dynamic controls ID has to be the same as the one you sent to the client in the first place. You subscribe to its Click or Command event and set its visibility to false. Inside the click event handler, you set the senders visibility to true again. This implies, that you don't care if that link is created but instead just don't want to send it to the client. The example below only works for a single link of course (but you could easily modify it to cover a whole group of links).
public void Page_Load(object sender, EventArgs e)
{
LinkButton dynamicButton = new LinkButton();
dynamicButton.ID = "linkDynamic123";
// this id needs to be the same as it was when you
// first sent the page containing the dynamic link to the client
dynamicButton.Click += DynamicButton_Click;
dynamicButton.Visible = false;
Controls.Add(dynamicButton);
}
public void DynamicButton_Click(object sender, EventArgs e)
{
// as you created the control during Page.Load, this event will be fired.
((LinkButton)sender).Visible = true;
}

Moving code to master page SavePageStateToPersistenceMedium & LoadPageStateFromPersistenceMedium

I want to remove the view state using the method below. My problem is I don't like it put the code below into almost every page. I like to move it to the masterpage or convert it to a class and run it from the masterpage.
The code below on works on the non-masterpage .
It will have this problem if I put inside the masterpage CS0115: 'MasterPage.SavePageStateToPersistenceMedium(object)': no suitable method found to override
#region Disable ViewState
protected override void SavePageStateToPersistenceMedium(object state)
{
}
protected override object LoadPageStateFromPersistenceMedium()
{
return null;
}
#endregion
<% EnableViewState="false" %>
you can make this EnableViewState="false" on top of every page, which will disable view state on that particular page, but if you want any control to have the view state enabled than you can enable it by the same EnableViewState="true" on that control.
You should put this in a base class and have each of your pages that you don't want viewstate on inherit from it
//a basepage that overrides the methods
public class BasePage : System.Web.UI.Page
{
protected override void SavePageStateToPersistenceMedium(object state)
{
}
protected override object LoadPageStateFromPersistenceMedium()
{
return null;
}
}
//your page class that inherits your base page
public class Page1 : BasePage
{
}
Each page has an option to not use ViewState.
<asp:Page EnableViewState="True|False" />
//using the below you will need to change asp:ImageButton coding method. You need to convert it to a regular submit button. There may be other problems which i don't now yet.
Place the code below on the master page.
protected override void Render(HtmlTextWriter writer)
{///Articles/How-to-disable-or-remove-ViewState-Hidden-Field-in-ASP.Net-Page.aspx
StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter(sb);
HtmlTextWriter ht = new HtmlTextWriter(sw);
base.Render(ht);
string html = sb.ToString();
//The remove the value and it id from the view status this next line
html = Regex.Replace(html, "<input[^>]*id=\"(__VIEWSTATE)\"[^>]*>", "<input type=\"hidden\" name=\"__VIEWSTATE\" value=\"\"/>", RegexOptions.IgnoreCase);
//To completely remove the view state use the line below
// html = Regex.Replace(html, "<input[^>]*id=\"(__VIEWSTATE)\"[^>]*>", "", RegexOptions.IgnoreCase);
writer.Write(html);
sb = null; html = null; ht.Dispose(); sw.Dispose();
}
Use this technique to detect the clicked button on your aspx page(not the master page). Place this code in the code behind
if (Request.Form["nonPostBackButton1.y"] != null)
{
Response.Write("nonPostBackButton1 is just pressed");
}
if (Request.Form["nonPostBackButton2.y"] != null)
{
Response.Write("<br/><b>nonPostBackButton2 is just pressed</b>");
}
This is the sample of the image submit buttons
<input type="image" alt="nonPostBackButton1" name="nonPostBackButton1"/> Use this method instead of the postback buttons. This is the image submit button 1</p>
<p><input type="image" alt="nonPostBackButton2" name="nonPostBackButton2"/> Use this method instead of the postback buttons. This is the image submit button 1</p>

Add ScriptManager to Page Programmatically?

I am developing a WebPart (it will be used in a SharePoint environment, although it does not use the Object Model) that I want to expose AJAX functionality in. Because of the nature of the environment, Adding the Script Manager directly to the page is not an option, and so must be added programmatically. I have attempted to add the ScriptManager control to the page in my webpart code.
protected override void CreateChildControls()
{
if (ScriptManager.GetCurrent(Page) == null)
{
ScriptManager sMgr = new ScriptManager();
// Ensure the ScriptManager is the first control.
Page.Form.Controls.AddAt(0, sMgr);
}
}
However, when this code is executed, I get the following error message:
"The control collection cannot be modified during DataBind, Init, Load, PreRender or Unload phases."
Is there another way to add the ScriptManager to the page from a WebPart, or am I going to have to just add the ScriptManager to each page (or master page) that will use the WebPart?
I was able to get this to work by using the Page's Init event:
protected override void OnInit(EventArgs e)
{
Page.Init += delegate(object sender, EventArgs e_Init)
{
if (ScriptManager.GetCurrent(Page) == null)
{
ScriptManager sMgr = new ScriptManager();
Page.Form.Controls.AddAt(0, sMgr);
}
};
base.OnInit(e);
}
I had the same basic issue the rest of you had. I was creating a custom ascx control and wanted to be able to not worry about whether or not the calling page had the scriptmanager declared. I got around the issues by adding the following to the ascx contorl itself.
to the ascx page -
<asp:PlaceHolder runat="server" ID="phScriptManager"></asp:PlaceHolder>
in the update panel itself - oninit="updatePanel1_Init"
to the ascx.cs file -
protected void updatePanel1_Init(object sender, EventArgs e)
{
if (ScriptManager.GetCurrent(this.Page) == null)
{
ScriptManager sManager = new ScriptManager();
sManager.ID = "sManager_" + DateTime.Now.Ticks;
phScriptManager.Controls.AddAt(0, sManager);
}
}
Thank you to everyone else in this thread who got me started.
I've done this and it works. Create a placeholder for the controls:
<asp:PlaceHolder ID="WebGridPlaceholder" runat="server" >
</asp:PlaceHolder>
Then you can do this in CreateChildControls:
ScriptManager aSM = new ScriptManager();
aSM.ID = "GridScriptManager";
WebGridPlaceholder.Controls.Add(aSM);
I ran into this problem with a custom ascx server control. I tried many solutions involving adding script to the OnInit events of the control (which doesn't get executed until after it checks for the ScriptManager control), adding logic inside of server tags on the control, and adding things to about every other event. No good. I finally built a control that inherits from ScriptManagerProxy and then uses ktrauberman's piece of code, slightly modified, to add a ScriptManager if needed:
public class ProxiedScriptManager : ScriptManagerProxy
{
protected override void OnInit(EventArgs e)
{
//double check for script-manager, if one doesn't exist,
//then create one and add it to the page
if (ScriptManager.GetCurrent(this.Page) == null)
{
ScriptManager sManager = new ScriptManager();
sManager.ID = "sManager_" + DateTime.Now.Ticks;
Controls.AddAt(0, sManager);
}
base.OnInit(e);
}
}
That did it for me.
This is the only way I could get my update panel to work in a sharepoint 2007 / 2010 compatible webpart. We use a 2010 master page with an scriptmanager but a 2007 master page without one.
.ascx
<asp:PlaceHolder ID="sMgr_place" runat="server" />
<asp:UpdatePanel runat="server" OnInit="updatePanel_Init"><ContentTemplate>
...
</ContentTemplate></asp:UpdatePanel>
.ascx.cs
public void updatePanel_Init(object sender, EventArgs e)
{
if (ScriptManager.GetCurrent(Page) == null)
{
ScriptManager sMgr = new ScriptManager();
sMgr.EnablePartialRendering = true;
sMgr_place.Controls.Add(sMgr);
}
}
I used this code in custom web controls (.cs) that contain update panels.
protected override void OnInit(EventArgs e)
{
//...
if (ScriptManager.GetCurrent(this.Page) == null)
{
ScriptManager scriptManager = new ScriptManager();
scriptManager.ID = "scriptManager_" + DateTime.Now.Ticks;
Controls.AddAt(0, scriptManager);
}
//...
}
I had this similar problem and found the best way was to add a global ScriptManager to the masterpage then in the code behind you can add to it by:
ScriptManager.GetCurrent(Page).Services.Add(new ServiceReference(virtualPath));

Categories