I am working on a Web Forms application and I have some HTML code that I need in 2-3 more places.
I've created .ascx control like so which represents the repeated code:
protected override void Render(HtmlTextWriter writer)
{
StringBuilder str = new StringBuilder();
while (connRef.Read())
{
str.Append(connRef["some_database_field"]);
}
writer.Write(str.ToString());
base.Render(writer);
}
connRef is a reference to DataReader object that I pass where I need this piece of code to render.
In other control, I use following code:
MSSqlConn s = new MSSqlConn();
StringBuilder str = new StringBuilder();
s.OpenConn("select * from notes order by note_date desc;");
notes.note c = (notes.note)Page.LoadControl(#"/controls/notes/note.ascx");
c.ID = "note";
c.connRef = s;
while (s.Read())
{
str.Append(Html.RenderControl(c));
}
s.CloseConn();
Response.Write(str.ToString());
MSSqlConn is my class for database connection.
RenderControl renders any control as a HTML string.
s in second code snippet returns only one record instead of two. For some reason, s closes if I pass the reference to another control (c.connRef = s).
Maybe I am missing something, I don't know.
I am sorry if I hadn't explain it well.
Your problem is that you're calling the Read() method twice. This is what your code is basically doing:
while (s.Read())
{
while (connRef.Read())
{
str.Append(connRef["some_database_field"]);
}
}
Every call to the Read() method advances the data reader by one record. You need to take out one of the while loops.
You should also note that if you're passing this data reader to multiple user controls, you will need to reset it to start from the beginning for each new user control.
Related
Background Info: I'm using an ItemCheckedIn receiver in SharePoint 2010, targeting .NET 3.5 Framework. The goal of the receiver is to:
Make sure the properties (columns) of the page match the data in a Content Editor WebPart on the page so that the page can be found in a search using Filter web parts. The pages are automatically generated, so barring any errors they are guaranteed to fit the expected format.
If there is a mismatch, check out the page, fix the properties, then check it back in.
I've kept the receiver from falling into an infinite check-in/check-out loop, although right now it's a very clumsy fix that I'm trying to work on. However, right now I can't work on it because I'm getting a DisconnectedContext error whenever I hit the UpdatePage function:
public override void ItemCheckedIn(SPItemEventProperties properties)
{
// If the main page or machine information is being checked in, do nothing
if (properties.AfterUrl.Contains("home") || properties.AfterUrl.Contains("machines")) return;
// Otherwise make sure that the page properties reflect any changes that may have been made
using (SPSite site = new SPSite("http://san1web.net.jbtc.com/sites/depts/VPC/"))
using (SPWeb web = site.OpenWeb())
{
SPFile page = web.GetFile(properties.AfterUrl);
// Make sure the event receiver doesn't get called infinitely by checking version history
...
UpdatePage(page);
}
}
private static void UpdatePage(SPFile page)
{
bool checkOut = false;
var th = new Thread(() =>
{
using (WebBrowser wb = new WebBrowser())
using (SPLimitedWebPartManager manager = page.GetLimitedWebPartManager(PersonalizationScope.Shared))
{
// Get web part's contents into HtmlDocument
ContentEditorWebPart cewp = (ContentEditorWebPart)manager.WebParts[0];
HtmlDocument htmlDoc;
wb.Navigate("about:blank");
htmlDoc = wb.Document;
htmlDoc.OpenNew(true);
htmlDoc.Write(cewp.Content.InnerText);
foreach (var prop in props)
{
// Check that each property matches the information on the page
string element;
try
{
element = htmlDoc.GetElementById(prop).InnerText;
}
catch (NullReferenceException)
{
break;
}
if (!element.Equals(page.GetProperty(prop).ToString()))
{
if (!prop.Contains("Request"))
{
checkOut = true;
break;
}
else if (!element.Equals(page.GetProperty(prop).ToString().Split(' ')[0]))
{
checkOut = true;
break;
}
}
}
if (!checkOut) return;
// If there was a mismatch, check the page out and fix the properties
page.CheckOut();
foreach (var prop in props)
{
page.SetProperty(prop, htmlDoc.GetElementById(prop).InnerText);
page.Item[prop] = htmlDoc.GetElementById(prop).InnerText;
try
{
page.Update();
}
catch
{
page.SetProperty(prop, Convert.ToDateTime(htmlDoc.GetElementById(prop).InnerText).AddDays(1));
page.Item[prop] = Convert.ToDateTime(htmlDoc.GetElementById(prop).InnerText).AddDays(1);
page.Update();
}
}
page.CheckIn("");
}
});
th.SetApartmentState(ApartmentState.STA);
th.Start();
}
From what I understand, using a WebBrowser is the only way to fill an HtmlDocument in this version of .NET, so that's why I have to use this thread.
In addition, I've done some reading and it looks like the DisconnectedContext error has to do with threading and COM, which are subjects I know next to nothing about. What can I do to prevent/fix this error?
EDIT
As #Yevgeniy.Chernobrivets pointed out in the comments, I could insert an editable field bound to the page column and not worry about parsing any html, but because the current page layout uses an HTML table within a Content Editor WebPart, where this kind of field wouldn't work properly, I'd need to make a new page layout and rebuild my solution from the bottom up, which I would really rather avoid.
I'd also like to avoid downloading anything, as the company I work for normally doesn't allow the use of unapproved software.
You shouldn't do html parsing with WebBrowser class which is part of Windows Forms and is not suited for web as well as for pure html parsing. Try using some html parser like HtmlAgilityPack instead.
I'm trying to create a custom Razor view base class (inheriting WebViewPage) that will inject a bit of HTML for each view template being rendered (including Layouts and Partial Views) so that I have a reference on the client of where each Razor template starts (not interested in where it ends).
What I have tried so far is
overriding the Write method (as described in a comment here) . This injects code at every razor section, not just once per template (for example every time that you use the HTML.TextBoxFor)
overriding the ExecutePageHierarchy method (as described in the post of the link above). This throws an error every time it hits the first PopContext call: The "RenderBody" method has not been called for layout page "~/Views/Shared/_Layout.cshtml".
after trying your solution, I had some problems with the rendered HTML of complex pages with partial views.
my issue was that everything was reversed. (order of partial views)
to correct - I ended up replacing the Output stream in the OutputStack
public override void ExecutePageHierarchy()
{
// Replace output stream with a fake local stream
StringWriter fakeOutput = new StringWriter();
// Save output stack top level stream, and replace with fake local stream
TextWriter outputStackTopOutput = OutputStack.Pop();
OutputStack.Push(fakeOutput);
// Run Razor view engine
base.ExecutePageHierarchy();
string content = fakeOutput.ToString();
// Set back real outputs, and write to the real output
OutputStack.Pop();
OutputStack.Push(outputStackTopOutput);
outputStackTopOutput.Write(content);
}
Think that I have an answer to this now:
public abstract class CustomWebViewPage: WebViewPage
{
public override void ExecutePageHierarchy()
{
var layoutReferenceMarkup = #"<script type=""text/html"" data-layout-id=""" + TemplateInfo.VirtualPath + #"""></script>";
base.ExecutePageHierarchy();
string output = Output.ToString();
//if the body tag is present the script tag should be injected into it, otherwise simply append
if (output.Contains("</body>"))
{
Response.Clear();
Response.Write(output.Replace("</body>", layoutReferenceMarkup+"</body>"));
Response.End();
}
else
{
Output.Write(layoutReferenceMarkup);
}
}
}
public abstract class CustomWebViewPage<TModel>: CustomWebViewPage
{
}
Seems to work, but if anyone has a better solution, please share.
In my WinForms application I need to call javascript function from my WebBrowser control. I used Document.InvokeScript and it works perfect with functions alone e.g
Document.InvokeScript("function").
But when i want to call javascript object method e.g.
Document.InvokeScript("obj.method")
it doesn't work. Is there a way to make it work? Or different solution to this problem? Without changing anything in the javascript code!
Thanks in advance :)
The example in the documentation does NOT include the parenthesis.
private void InvokeScript()
{
if (webBrowser1.Document != null)
{
HtmlDocument doc = webBrowser1.Document;
String str = doc.InvokeScript("test").ToString() ;
Object jscriptObj = doc.InvokeScript("testJScriptObject");
Object domOb = doc.InvokeScript("testElement");
}
}
Try
Document.InvokeMethod("obj.method");
Note that you can pass arguments if you use HtmlDocument.InvokeScript Method (String, Object[]).
Edit
Looks like you aren't the only one with this issue: HtmlDocument.InvokeScript - Calling a method of an object . You can make a "Proxy function" like the poster of that link suggests. Basically you have a function that invokes your object's function. It's not an ideal solution, but it'll definitely work. I'll continue looking to see if this is possible.
Another post on same issue: Using WebBrowser.Document.InvokeScript() to mess around with foreign JavaScript . Interesting solution proposed by C. Groß on CodeProject:
private string sendJS(string JScript) {
object[] args = {JScript};
return webBrowser1.Document.InvokeScript("eval",args).ToString();
}
You could make that an extension method on HtmlDocument and call that to run your function, only using this new function you WOULD include parenthesis, arguments, the whole nine yards in the string you pass in (since it is just passed along to an eval).
Looks like HtmlDocument does not have support for calling methods on existing objects. Only global functions. :(
Unfortunately you can't call object methods out of the box using WebBrowser.Document.InvokeScript.
The solution is to provide a global function on the JavaScript side which can redirect your call. In the most simplistic form this would look like:
function invoke(method, args) {
// The root context is assumed to be the window object. The last part of the method parameter is the actual function name.
var context = window;
var namespace = method.split('.');
var func = namespace.pop();
// Resolve the context
for (var i = 0; i < namespace.length; i++) {
context = context[namespace[i]];
}
// Invoke the target function.
result = context[func].apply(context, args);
}
In your .NET code you would use this as follows:
var parameters = new object[] { "obj.method", yourArgument };
var resultJson = WebBrowser.Document.InvokeScript("invoke", parameters);
As you mention that you cannot change anything to your existing JavaScript code, you'll have to inject the above JavaScript method in some how. Fortunately the WebBrowser control can also do for you by calling the eval() method:
WebBrowser.Document.InvokeScript("eval", javaScriptString);
For a more robust and complete implementation see the WebBrowser tools I wrote and the article explaining the ScriptingBridge which specifically aims to solve the problem you describe.
webBrowser.Document.InvokeScript("execScript", new object[] { "this.alert(123)", "JavaScript" })
for you supposed to be like this
webBrowser.Document.InvokeScript("execScript", new object[] { "obj.method()", "JavaScript" })
Good day
I have question about displaying html documents in a windows forms applications. App that I'm working on should display information from the
database in the html format. I will try to describe actions that I have taken (and which failed):
1) I tried to load "virtual" html page that exists only in memory and dynamically change it's parameters (webbMain is a WebBrowser control):
public static string CreateBookHtml()
{
StringBuilder sb = new StringBuilder();
//Declaration
sb.AppendLine(#"<?xml version=""1.0"" encoding=""utf-8""?>");
sb.AppendLine(#"<?xml-stylesheet type=""text/css"" href=""style.css""?>");
sb.AppendLine(#"<!DOCTYPE html PUBLIC ""-//W3C//DTD XHTML 1.1//EN""
""http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"">");
sb.AppendLine(#"<html xmlns=""http://www.w3.org/1999/xhtml"" xml:lang=""en"">");
//Head
sb.AppendLine(#"<head>");
sb.AppendLine(#"<title>Exemplary document</title>");
sb.AppendLine(#"<meta http-equiv=""Content-Type"" content=""application/xhtml+xml;
charset=utf-8""/ >");
sb.AppendLine(#"</head>");
//Body
sb.AppendLine(#"<body>");
sb.AppendLine(#"<p id=""paragraph"">Example.</p>");
sb.AppendLine(#"</body>");
sb.AppendLine(#"</html>");
return sb.ToString();
}
void LoadBrowser()
{
this.webbMain.Navigate("about:blank");
this.webbMain.DocumentText = CreateBookHtml();
HtmlDocument doc = this.webbMain.Document;
}
This failed, because doc.Body is null, and doc.getElementById("paragraph") returns null too. So I cannot change paragraph InnerText property.
Furthermore, this.webbMain.DocumentText is "\0"...
2) I tried to create html file in specified folder, load it to the WebBrowser and then change its parameters. Html is the same as created by
CreateBookHtml() method:
private void LoadBrowser()
{
this.webbMain.Navigate("HTML\\BookPage.html"));
HtmlDocument doc = this.webbMain.Document;
}
This time this.webbMain.DocumentText contains Html data read from the file, but doc.Body returns null again, and I still cannot take element using
getByElementId() method. Of course, when I have text, I would try regex to get specified fields, or maybe do other tricks to achieve a goal, but I wonder - is there simply way to mainipulate html? For me, ideal way would be to create HTML text in memory, load it into the WebBrowser control, and then dynamically change its parameters using IDs. Is it possible? Thanks for the answers in advance, best regards,
Paweł
I've worked some time ago with the WebControl and like you wanted to load a html from memory but have the same problem, body being null. After some investigation, I noticed that the Navigate and NavigateToString methods work asynchronously, so it needs a little time for the control to load the document, the document is not available right after the call to Navigate. So i did something like (wbChat is the WebBrowser control):
wbChat.NavigateToString("<html><body><div>first line</div></body><html>");
DoEvents();
where DoEvents() is implemeted as:
[SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.UnmanagedCode)]
public void DoEvents()
{
DispatcherFrame frame = new DispatcherFrame();
Dispatcher.CurrentDispatcher.BeginInvoke(DispatcherPriority.Background,
new DispatcherOperationCallback(ExitFrame), frame);
Dispatcher.PushFrame(frame);
}
and it worked for me, after the DoEvents call, I could obtain a non-null body:
mshtml.IHTMLDocument2 doc2 = (mshtml.IHTMLDocument2)wbChat.Document;
mshtml.HTMLDivElement div = (mshtml.HTMLDivElement)doc2.createElement("div");
div.innerHTML = "some text";
mshtml.HTMLBodyClass body = (mshtml.HTMLBodyClass)doc2.body;
if (body != null)
{
body.appendChild((mshtml.IHTMLDOMNode)div);
body.scrollTop = body.scrollHeight;
}
else
Console.WriteLine("body is still null");
I don't know if this is the right way of doing this, but it fixed the problem for me, maybe it helps you too.
Later Edit:
public object ExitFrame(object f)
{
((DispatcherFrame)f).Continue = false;
return null;
}
The DoEvents method is necessary on WPF. For System.Windows.Forms one can use Application.DoEvents().
Another way to do the same thing is:
webBrowser1.DocumentText = "<html><body>blabla<hr/>yadayada</body></html>";
this works without any extra initialization
I have an aspx page that contains regular html, some uicomponents, and multiple tokens of the form {tokenname} .
When the page loads, I want to parse the page content and replace these tokens with the correct content. The idea is that there will be multiple template pages using the same codebehind.
I've no trouble parsing the string data itself, (see named string formatting, replace tokens in template) my trouble lies in when to read, and how to write the data back to the page...
What's the best way for me to rewrite the page content? I've been using a streamreader, and the replacing the page with Response.Write, but this is no good - a page containing other .net components does not render correctly.
Any suggestions would be greatly appreciated!
Take a look at System.Web.UI.Adapters.PageAdapter method TransformText - generally it is used for multi device support, but you can postprocess your page with this.
I'm not sure if I'm answering your question, but...
If you can change your notation from
{tokenname}
to something like
<%$ ZeusExpression:tokenname %>
you could consider creating your System.Web.Compilation.ExpressionBuilder.
After reading your comment...
There are other ways of getting access to the current page using ExpressionBuilder: just... create an expression. ;-)
Changing just a bit the sample from MSDN and supposing the code of your pages contain a method like this
public object GetData(string token);
you could implement something like this
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
Type type1 = entry.DeclaringType;
PropertyDescriptor descriptor1 = TypeDescriptor.GetProperties(type1)[entry.PropertyInfo.Name];
CodeExpression[] expressionArray1 = new CodeExpression[1];
expressionArray1[0] = new CodePrimitiveExpression(entry.Expression.Trim());
return new CodeCastExpression(
descriptor1.PropertyType,
new CodeMethodInvokeExpression(
new CodeThisReferenceExpression(),
"GetData",
expressionArray1));
}
This replaces your placeholder with a call like this
(string)this.GetData("tokenname");
Of course you can elaborate much more on this, perhaps using a "utility method" to simplify and "protect" access to data (access to properties, no special method involved, error handling, etc.).
Something that replaces instead with (e.g.)
(string)Utilities.GetData(this, "tokenname");
Hope this helps.
Many thanks to those that contributed to this question, however I ended up using a different solution -
Overriding the render function as per this page, except I parsed the page content for multiple different tags using regular expressions.
protected override void Render(HtmlTextWriter writer)
{
if (!Page.IsPostBack)
{
using (System.IO.MemoryStream stream = new System.IO.MemoryStream())
{
using (System.IO.StreamWriter streamWriter = new System.IO.StreamWriter(stream))
{
HtmlTextWriter htmlWriter = new HtmlTextWriter(streamWriter);
base.Render(htmlWriter);
htmlWriter.Flush();
stream.Position = 0;
using (System.IO.StreamReader oReader = new System.IO.StreamReader(stream))
{
string pageContent = oReader.ReadToEnd();
pageContent = ParseTagsFromPage(pageContent);
writer.Write(pageContent);
oReader.Close();
}
}
}
}
else
{
base.Render(writer);
}
}
Here's the regex tag parser
private string ParseTagsFromPage(string pageContent)
{
string regexPattern = "{zeus:(.*?)}"; //matches {zeus:anytagname}
string tagName = "";
string fieldName = "";
string replacement = "";
MatchCollection tagMatches = Regex.Matches(pageContent, regexPattern);
foreach (Match match in tagMatches)
{
tagName = match.ToString();
fieldName = tagName.Replace("{zeus:", "").Replace("}", "");
//get data based on my found field name, using some other function call
replacement = GetFieldValue(fieldName);
pageContent = pageContent.Replace(tagName, replacement);
}
return pageContent;
}
Seems to work quite well, as within the GetFieldValue function you can use your field name in any way you wish.