I didn't think this would be that hard, but I am trying to pass a class that has a dynamic for a property (which is currently being set as an expando object in the c#) into the View. In this view, a good bit is getting rendered w/ some jQuery Templating and I thought that if I had this in the c# code:
public dynamic SomeProperty {get;set;}
...
SomeProperty = new ExpandoObject();
SomeProperty.SomeValue = "5";
return View(TheClassThatContainsSomeProperty);
Such that in the jquery templating I could do:
${SomeProperty.SomeValue}
...and that it would hopefully work. It doesn't... If you inspect the response, you can see that it essentially gets sent over as a dictionary:
SomeProperty: [{SomeValue, Value:5}]
0: {Key:SomeValue, Value:5}
which leads to (I guess) my next question: Is there an easy way to access dictionaries in jquery templating? I did try this:
${SomeProperty["SomeValue"]}
to no avail either. At this point the only thing I know to do is to leverage the ability to put a function in the template (as copied here from the jquery website):
Template:
<tr><td>${getLanguages(Languages, " - ")}</td></tr>
Code:
function getLanguages( data, separator ) {
return data.join( separator );
}
So am I over complicating this? Can I easily either 1) access a dynamic value from jquery template or 2) Easily lookup a value from a dictionary in jquery template?
ExpandoObject derives from IEnumerable<KeyValuePair<string, Object>>, and most serializers will recognize a dynamic as this type when you assign an ExpandoObject. This is why you see an array type on the javascript side with named Key::Value pairs.
ExpandoObject Class (System.Dynamic) # MSDN
One alternative to using ExpandoObject is to use C# anonymous types. When serialized to json, these map field by field as you expect.
Anonymous Types (C# Programming Guide) # MSDN
It is possible to access values declared with dynamic from jQuery, but most likely you won't be returning a MVC View() with the model to be consumed with jQuery, as any server-side view template engine (razor, etc.) can already perform the same template activities with less overhead. Instead, jQuery templates are better used with Ajax calls.
Here are code examples demonstrating three cases where a variable declared dynamic on the server is consumed with jQuery templates in the browser.
The first example uses an anonymous type for the member field SomeValue, and has a jQuery template that treats it as a member object.
The second example uses an array of anonymous types for the member field SomeValue and has a template that uses {{each}} syntax to enumerate the items. Note that this is a scenario where things can go badly with dynamic, as you get no strongly-typed support and must either know the correct type or discover it at the time you access it.
The third example uses an ExpandoObject for the member field SomeValue, and has a jQuery template like the first example (single member object). Note that in this case, we need to use a helper function pivotDictionaryMap() to pivot Key::Value pairs into object members.
Starting with a blank C# MVC3 Web Project, we need to modify three files to demonstrate these examples.
Inside _Layout.cshtml, add script includes for jQuery templates and a proper version of jQuery in your <head> element.
<script src="#Url.Content("~/Scripts/jquery-1.8.2.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.tmpl.js")" type="text/javascript"></script>
Inside HomeController.cs, we'll add some methods that return json ActionResults. Also, for brevity we'll just declare a class SomeModelType here; and note that a proper application would probably have this class declared in its Models.
using System.Dynamic; // up top...
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
return View();
}
public ActionResult SomeDataSource(int id)
{
dynamic d = new { innerId = 99, innerLabel = "inside object" };
SomeModelType obj = new SomeModelType(id, "new object");
obj.SomeValue = d;
return Json(obj, "text/plain");
}
public ActionResult SomeDataSourceWithArray(int id)
{
dynamic d1 = new { innerId = 99, innerLabel = "inside object (first array member)" };
dynamic d2 = new { innerId = 100, innerLabel = "inside object (second array member)" };
SomeModelType obj = new SomeModelType(id, "new object");
obj.SomeValue = new object[] {d1, d2};
return Json(obj, "text/plain");
}
public ActionResult SomeDataSourceWithExpando(int id)
{
dynamic d = new ExpandoObject();
d.innerId = 99;
d.innerLabel = "inside object";
SomeModelType obj = new SomeModelType(id, "new object");
obj.SomeValue = d;
return Json(obj, "text/plain");
}
}
public class SomeModelType
{
public SomeModelType(int initId, string initLabel)
{
Id = initId;
Label = initLabel;
}
public int Id { get; set; }
public string Label { get; set; }
public dynamic SomeValue { get; set; }
}
Finally, in the default view, we will add script tags for the templates and the javascript necessary to consume them. Note the use of $.post() and not $.get(), as a JsonResult in MVC disallows GET requests by default (you can turn these on with an attribute).
#{
ViewBag.Title = "Home Page";
}
<h2>#ViewBag.Message</h2>
<script id="someDataTemplate" type="text/x-jquery-tmpl">
Item <b>${Id}</b> is labeled "<i>${Label}</i>" and has an inner item with id <b>${SomeValue.innerId}</b> whose label is "<i>${SomeValue.innerLabel}</i>".
</script>
<h3>SomeDataSource Example #1 (Single Item)</h3>
<div id="someData">
</div>
<script id="someDataArrayTemplate" type="text/x-jquery-tmpl">
Item <b>${Id}</b> is labeled "<i>${Label}</i>" and has these inner items:
<ul>
{{each SomeValue}}
<li><b>${innerId}</b> has a label "<i>${innerLabel}</i>".</li>
{{/each}}
</ul>
</script>
<h3>SomeDataSource Example #2 (Array)</h3>
<div id="someArrayData">
</div>
<script id="someDataTemplateFromExpandoObject" type="text/x-jquery-tmpl">
Item <b>${Id}</b> is labeled "<i>${Label}</i>" and has an inner item with id <b>${SomeValue.innerId}</b> whose label is "<i>${SomeValue.innerLabel}</i>".
</script>
<h3>SomeDataSource Example #3 (Single Item, Expando Object)</h3>
<div id="someDataFromExpandoObject">
</div>
<script type="text/javascript">
function pivotDictionaryMap(src)
{
var retval = {};
$.each(src, function(index, item){
retval[item.Key] = item.Value;
});
return retval;
}
</script>
<script type="text/javascript">
$(document).ready(function() {
// Ajax Round-Trip to fill example #1
$.post("/Home/SomeDataSource/5", function(data) {
$("#someDataTemplate").tmpl(data).appendTo("#someData");
}, "json");
// Ajax Round-Trip to fill example #2
$.post("/Home/SomeDataSourceWithArray/67", function(data) {
$("#someDataArrayTemplate").tmpl(data).appendTo("#someArrayData");
}, "json");
// Ajax Round-Trip to fill example #3
$.post("/Home/SomeDataSourceWithExpando/33", function(data) {
data.SomeValue = pivotDictionaryMap(data.SomeValue);
$("#someDataTemplateFromExpandoObject").tmpl(data).appendTo("#someDataFromExpandoObject");
}, "json");
});
</script>
I won't mark my own answer as "correct" just on premise that I don't like this answer. If someone figures out what I was trying to do above, I'll gladly give you the points.
function getDisplayValue(data, toMatchOn) {
var _return = $.grep(data, function (n, i) { return (n.Key == toMatchOn); })[0];
if (_return != null)
return _return.Value;
return "";
}
and in the jquery template:
${getDisplayValue(DisplayFields, 'Something')}
So I was able to get this to work with the method described above so here it is as a possible solution but I don't know javascript well enough to know how bad of a performance hit this could be creating (and as I research it, I'll update this answer) but I thought javascript dictionaries were optimized against their key value, so the fact that MVC doesn't serialize expando as a true javascript dictionary seems to make this answer very inefficient. And the fact that I originally took this tack with the dynamic c# object was that I originally thought this would serialize down into a cleaner form. Anyway, this works but Occam's Razor is just making this feel way too complicated.
Not sure if this will help or not, but have a look at this gist. It is hard to tell from your code snippets but if you are turning that ExpandoObject into JSON, then try wrapping it on a DynamicJsonObject first, as done in the gist.
Code from the Gist copy/pasted for those who don't want to click the link:
// By default, Json.Encode will turn an ExpandoObject into an array of items,
// because internally an ExpandoObject extends IEnumerable<KeyValuePair<string, object>>.
// You can turn it into a non-list JSON string by first wrapping it in a DynamicJsonObject.
[TestMethod]
public void JsonEncodeAnExpandoObjectByWrappingItInADynamicJsonObject()
{
dynamic expando = new ExpandoObject();
expando.Value = 10;
expando.Product = "Apples";
var expandoResult = System.Web.Helpers.Json.Encode(expando);
var dictionaryResult = System.Web.Helpers.Json.Encode(new DynamicJsonObject(expando));
Assert.AreEqual("{\"Value\":10,\"Product\":\"Apples\"}", expandoResult); // FAILS (encodes as an array instead)
Assert.AreEqual("{\"Value\":10,\"Product\":\"Apples\"}", dictionaryResult); // PASSES
}
You're right that performance is going to be sketchy if you're iterating over an array to find a key. But you should be able to comfortably convert the key-value-pair array (that the server sends back) into a "true" Javascript dictionary/map. Eg:
var kvps = [ {key: "test", value: "expando"}, {key: "hello", value: "world" } ];
var map = {};
kvps.forEach(function(kvp) { map[kvp.key] = kvp.value; } );
console.log( JSON.stringify(map) );
{"test":"expando","hello":"world"}
Of course, if you're nested objects, then you'd have to apply recursion to the above approach to make it work.
Related
I am using the Essential objects library to read out websites.
I've done that before with windows forms webbrowser, but this time the website is not working with windows forms webbrowser so I had to change to EO webView.
The documentary is so poor, that I can't find an answer.
In windows forms webbrowser you have a HtmlElementCollection which is in principle a list of HtmlElement.
On these elements you can read out attributes or make an InvokeMember("Click") and navigate through children / parent elements.
what is the equivalent in EO WebView to this HtmlElementCollection / HtmlElement?
How can I navigate through the HTML tree?
BTW: I am using it together with C#.
See the documentation: here, here, here.
Essentially, you have to rely on the ability to execute JavaScript.
You can access the document JavaScript object in a couple of ways:
JSObject document = (JSObject)_webView.EvalScript("document");
//or: Document document = _webView.GetDOMWindow().document;
GetDOMWindow() returns a EO.WebBrowser.DOM.Document instance; that type derives from JSObject and offers some extra properties (e.g., there's a body property that gets you the BODY element of type EO.WebBrowser.DOM.Element).
But overall, the API these offer is not much richer.
You can use JSObject like this:
// access a property on the JavaScript object:
jsObj["children"]
// access an element of an array-like JavaScript object:
var children = (JSObject)jsObj["children"];
var first = (JSObject)children[0];
// (note that you have to cast; all these have the `object` return type)
// access an attribute on the associated DOM element
jsObj.InvokeFunction("getAttribute", "class")
// etc.
It's all a bit fiddly, but you can write some extension methods to make your life easier (however, see the note on performance below):
public static class JSObjectExtensions
{
public static string GetTagName(this JSObject jsObj)
{
return (jsObj["tagName"] as string ?? string.Empty).ToUpper();
}
public static string GetID(this JSObject jsObj)
{
return jsObj["id"] as string ?? string.Empty;
}
public static string GetAttribute(this JSObject jsObj, string attribute)
{
return jsObj.InvokeFunction("getAttribute", attribute) as string ?? string.Empty;
}
public static JSObject GetParent(this JSObject jsObj)
{
return jsObj["parentElement"] as JSObject;
}
public static IEnumerable<JSObject> GetChildren(this JSObject jsObj)
{
var childrenCollection = (JSObject)jsObj["children"];
int childObjectCount = (int)childrenCollection["length"];
for (int i = 0; i < childObjectCount; i++)
{
yield return (JSObject)childrenCollection[i];
}
}
// Add a few more if necessary
}
Then you can do something like this:
private void TraverseElementTree(JSObject root, Action<JSObject> action)
{
action(root);
foreach(var child in root.GetChildren())
TraverseElementTree(child, action);
}
Here's an example of how you could use this method:
TraverseElementTree(document, (currentElement) =>
{
string tagName = currentElement.GetTagName();
string id = currentElement.GetID();
if (tagName == "TD" && id.StartsWith("codetab"))
{
string elementClass = currentElement.GetAttribute("class");
// do something...
}
});
But, again, it's a bit fiddly - while this seems to work reasonably well, you'll need to experiment a bit to find any tricky parts that can result in errors, and figure out how to modify the approach to make it more stable.
Note on performance
Another alternative is to use JavaScript for most of the element processing, and just return the values you need to be used in your C# code. Depending on how complex the logic is, this is likely going to be more efficient in certain scenarios, as it would result in a single browser engine round trip, so it's something to consider if performance becomes an issue. (See the Performance section here.)
Since this is one of the rare questions handling the element/child issue of the EO.WebBrowser and since I'm more into VB.NET, I was struggling to get the above working. It took me quite some hours to figure it out, so for everybody who is also more into VB.NET, here are some working VB equivalents.
Accessing your document and retrieving an element with children:
win = WebView1.GetDOMWindow()
doc = win.document
Dim element As EO.WebBrowser.DOM.Element = doc.getElementById("ddlSamples")
Access an element of an array-like object:
Dim children = CType(element("children"), EO.WebBrowser.JSObject)
Dim childrenCount As Integer = CInt(children("length"))
Get e.g. the selected OPTION-child within the element and get the innerText of the child:
Dim child As EO.WebBrowser.JSObject
Dim txt As String = Nothing
For c = 0 To childrenCount - 1
child = CType(children(c), EO.WebBrowser.JSObject)
If GetAttribute(child, "selected") = "selected" Then
txt = CStr(child("text"))
Exit For
End If
Next
And ofcourse declaring the 'GetAttribute' function:
Public Shared Function GetAttribute(ByVal jsObj As EO.WebBrowser.JSObject, ByVal attribute As String) As String
Return If(TryCast(jsObj.InvokeFunction("getAttribute", attribute), String), String.Empty)
End Function
Hopefully people will benefit from it.
I am struggling a bit with passing list of object to C# code from view. If I pass a simple string it works fine. but its not working for List. So I am sure I am missing something here.
View
<div class="row control-actions">
#{
List<MyObject> test = ViewBag.ObjectsList;
<button type="button" class="btn btn-primary btn-wide" onclick="addAllObjectsToExp('#test')">Add All</button>
}
</div>
<script type="text/javascript">
function addAllObjectsToExp(objList) {
$.post('#Url.Action("AddAllObjectsToExp", "ExportObjects")', { objectsList: objList},
function (result) {
$('#numbers').html(result);
});
}
</script>
Code
[HttpPost]
[OutputCache(Location = System.Web.UI.OutputCacheLocation.None, NoStore = false, Duration = 0)]
public int AddAllObjectsToExp(List<MyObject> objectsList)
{
foreach(MyObject obj in objectList)
{
//Do something here
}
//and return an integer
}
While debugging I can see that the #test variable is getting populated with the list of MyObject. But when I reach the code side its always null.
Please let me know if i am missing something here. Also tell me if more information is needed.
You're passing a C# object into a Javascript function. It doesn't know how to read that. You should serialize it into JSON before passing it in.
If you use Json.NET you can do it by
ViewBag.ObjectsList = JsonConvert.SerializeObject(yourlist);
Then you can continue as you were.
Some notes:
You should try to start using ViewModels instead of putting things in the ViewBag. On the Javascript side you should bind event handlers for things like clicking instead of using onclick as it would make your code much more manageable and reusable.
I have a method that is looping through a list of values, what I would like to do is when I open the page to be able to see the values changing without refreshing the current view. I've tried something like the code bellow.
public static int myValueReader { get; set; }
public static void ValueGenerator()
{
foreach (var item in myList)
{
myValue = item;
Thread.Sleep(1000);
}
}
The actual thing is I want it to read this values even if I close the form. I presume I would need to assign a Task in order to do this, but I was wandering if there is any better way of doing it since it's a MVC application?
Here's another way to do it:
use AJAX and setTimeout
declare one action in your controller (this one will return your different values)
an integer in your ViewBag, some like: ViewBag.totalItems
Declare an action in your controller: This is important, because this will be your connection with your database, or data. This action will receive the itemIndex and will return that item. Something like this:
[HttpPost]
public JsonResult GetItem(int index) {
return Json(myList.ElementAt(index));
}
ViewBag.TotalItems: Your view has to know how many Items you have in your list. I recommend you to pass that value as an integer via ViewBag:
public ActionResult Index() {
ViewBag.TotalItems = myList.Count();
return View();
}
AJAX and setTimeout: Once that you have all of this, you're ready to update your view without refreshing:
<script>
$(function() {
var totalItems = #Html.Raw(Json.Encode(ViewBag.TotalItems));
var currentItemIndex = 0;
var getData = function() {
$.post("#Url.Action("GetItem")", {index:currentItemIndex}, function(data) {
// data is myList.ElementAt(index)
// do something with it
}).always(function() {
currentItemIndex++;
if(currentItemIndex < totalItems) {
setTimeout(getData, 1000); // get the next item every 1 sec
}
})
}
getData(); // start updating
})
</script>
Your best bet as #DavidTansey mentioned is to use SignlarR. It wraps web sockets and falls back to long polling/etc if the users' browser doesn't support it. Your users will subscribe to specific channels and then you can raise events in those channels.
With regard to your business logic, you'll need to look into async programming techniques. Once you start on this, you'll probably have more specific questions.
I want to create a multilingual webpage. To switch between languages I've got a dropdown on my page. If the change event of the dropdown gets fired the Method called "ChangeLanguage" in my Controller is called.
public ViewModels.HomeViewModel HVM { get; private set; }
// GET: Home
public ActionResult Index()
{
this.HVM = new ViewModels.HomeViewModel();
return View(this.HVM);
}
public JsonResult ChangeLanguage(int id) {
return Json(new {Success = true});
}
Now I'd like to to change my "SelectedLanguage" Property in my ViewModel (HVM) - but the Reference is null. May anyone explain why HVM is null in my ChangeLanguage Method?
After my SelectedLanguage Property is changed I'd like to reload my whole page to display it's texts in another language
e.g.
#model ViewModels.HomeViewModel
<html>
<div class="HeaderText">
Text = #{
#Model.TextToDisplay.Where(o =>
o.Language.Equals(Model.SelectedLanguage)).First()
}
</div>
Here's what I want to do in PseudoCode:
PseudoCode:
public JsonResult ChangeLanguage(int id) {
this.HVM.SelectedLanguage =
this.HVM.AvailableLanguages.Where(o =>
o.ID.Equals(id)).First();
Page.Reload();
return Json(new {Success = true});
}
May anyone explain why HVM is null in my ChangeLanguage Method?
Adhering to stateless nature of HTTP protocol, all (unless explicitly added into request header) requests (MVC method calls) loose state data associated with it. Web server treats every request a new request and creates new instances of classes right from controller itself.
In your case since it is a new request, controller has a HVM property defined but in ChangeLanguage it is not instantiated (it gets instantiated only into Index method which is not called when you invoke ChangeLanguage) hence it is null.
After my SelectedLanguage Property is changed I'd like to reload my
whole page to display it's texts in another language.
Option 1: Refresh page
Simple option to implement. Pass the language selection to server, server will return a new view with specific data. Drawback, whole page will refresh.
Option 2: Update view selectively
If option 1 is really not acceptable, then consider this option. There are multiple ways you can achieve it. Basically it involves either (a) breaking you view into partial view and update only the portion that is affect by selection or (b) bind data element with a JS object.
(a) - Not much need to be said for this.
(b) - Data binding can easily be done if you employ a JS library like KnockoutJS.
Change your methods to these methods , This trick will work for you =>pass your model to Change language from view. Also update JsonResult to ActionResult.
public ActionResult ChangeLanguage(ViewModels.HomeViewModel model,int id)
{
this.HVM.SelectedLanguage =
this.HVM.AvailableLanguages.Where(o =>
o.ID.Equals(id)).First();
return RedirectToAction("Index",model);
}
public ActionResult Index(ViewModels.HomeViewModel model)
{
if(model == null)
{
this.HVM = new ViewModels.HomeViewModel();
}
return View(this.HVM);
}
I have an asp.net user control which exposes a public IEnumerable object. This could be converted to a list or array if it helps to answer the question. What I would like to do is to loop through all of the server objects within a javascript function and do something with the contents of each item. I would like to achieve this using inline server tags if possible. Something like the below.
function iterateServerCollection()
{
foreach(<%=PublicCollection %>)
{
var somevalue = <%=PublicCollection.Current.SomeValue %>;
}
}
Is it possible to achieve this?
Managed to achieve what I wanted thanks to the comment from geedubb. Here's what the working javascript looks like.
var myCollection = <%= new System.Web.Script.Serialization.JavaScriptSerializer().Serialize(myCollection) %>;
for(var i in myCollection)
{
var somevalue = myCollection[i].SomeValue;
}
I would do as others have suggested and serialize the object, you could even use an ajax call to grab the data from the start of that function
function iterateServerCollection()
{
//Ajax call to get data from server HttpHandler/WebAPI/Service etc.
for(var item in resultObject) {
//you can use item.WhateverPropertyIsOnObject
}
}