I have an asp.net-mvc app using angular. I had a get method that returns some data from the server. What I was returning was a Tuple with a status message and a second piece of data that was either an error message or the actual data. Ex:
return Json(new Tuple<string, string>("error", "bad request"));
//or
return Json(new Tuple<string, MyData>("success", new MyData());
this was working fine, in angular I did the following:
$http.get(url).then(
function (result) {
return result.data;
},
function (result) {
$q.reject(result.data);
}
here, result.data.Item1 was the first item of my Tuple
However, I changed my return types from Tuple to a new custom type that I created that looks like the following:
public class ServerResponse <T1, T2, T3>
{
T1 status { get; set; }
T2 message { get; set; }
T3 data { get; set; }
public ServerResponse(T1 status, T2 message, T3 data)
{
this.status = status;
this.message = message;
this.data = data;
}
}
but now when I do:
result.data.status
I dont get expected results because result.data is returning something like this:
__proto__ : Object
and I'm not sure how to fix this.
after looking around and comparing to the Tuple class, I noticed that my ServerResponse class's properties were not marked public so they weren't being found. After making them public the problem went away
Related
Question
I have a handful of ViewComponents that look like so:
public IViewComponentResult Invoke(BuyerViewModel buyer)
I'd like them to be able to accept either a BuyerViewModel, or a JSON string representing a BuyerViewModel. For example, when you pass JSON to a controller method from JavaScript, if that method expects an argument of type Dog, the controller automatically attempts to deserialize the JSON to an instance of Dog. I'm trying to mimic that behavior.
The goal would be that both of these examples work:
var buyer = new BuyerSummaryViewModel() { FirstName = "John" };
ViewComponent("Buyer", buyer);
ViewComponent("Buyer", "{\"Name\":\"John Smith\"}");
Why?
I'm trying to make a generic JavaScript method that can fetch a ViewComponent on the fly:
const fetchViewComponent = async (viewComponentName, viewModel) => {
let data = { viewComponentName, viewModel };
let html = await $.get(`/Order/FetchViewComponent`, data);
return html;
}
//Get a BuyerViewComponent (example)
(async () => {
let component = await fetchViewComponent("Buyer", `#Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model.Buyer))`);
console.log(component);
})();
What I've Tried
If I specify that the ViewModel is a BuyerViewModel, it works. The JSON string is automatically deserialized into a BuyerViewModel.
public class FetchViewComponentRequest
{
public string ViewComponentName { get; set; }
public BuyerViewModel ViewModel { get; set; }
// ^^^^^^^^^^^^^^
}
[HttpGet]
public IActionResult FetchViewComponent(FetchViewComponentRequest request)
{
return ViewComponent(request.ViewComponentName, request.ViewModel);
}
The Issue
However, I don't want to specify the type; I want this to be generic. So I tried this:
public class FetchViewComponentRequest
{
public string ViewComponentName { get; set; }
public string ViewModel { get; set; }
// ^^^^^^
}
[HttpGet]
public IActionResult FetchViewComponent(FetchViewComponentRequest request)
{
return ViewComponent(request.ViewComponentName, request.ViewModel);
}
But as expected, request.ViewModel isn't the correct type; it ends up null in the Invoke method. I was hoping there was a flag or something more global I could specify so that it tries to implicitly deserialize this string into the expected type.
Is there an easier way to do this that I haven't considered? Or, if not, is the way I'm envisioning even possible?
(I'm using .NET Core 2.2)
Maybe make your FetchViewComponentRequest generic?
public class FetchViewComponentRequest<T>
{
public string ViewComponentName { get; set; }
public T ViewModel { get; set; }
// ^^^^^^^^^^^^^^
}
[HttpGet]
public IActionResult FetchViewComponent(FetchViewComponentRequest<BuyerViewModel> request)
{
return ViewComponent(request.ViewComponentName, request.ViewModel);
}
The method needs to have some knowledge of what type to make the object coming in.
public T Convert<T>(dynamic obj) where T:class,new()
{
T myob = null;
if (obj !=null && obj is T)
{
myob = obj as T;
}
else if (obj is string)
{
//convert to type
myob = JsonConvert.DeserializeObject<T>(obj);
}
return myob;
}
Ok, im not sure about what you need.
But here is a dynamic way to do it, without specifying <T>.
//Assume that the namespace is DynamicTypeDemo
public class DynamicType {
// eg "DynamicTypeDemo.Cat, DynamicTypeDemo"
public string TypeName { get; set; } // the full path to the type
public string JsonString { get; set; }
}
Now you could simple DeserializeObject
public object ToObject(DynamicType dynamicType){
var type = Type.GetType(dynamicType.TypeName);
// Here you could check if the json is list, its really upp to you
// but as an example, i will still add it
if (dynamicType.JsonString.StartsWith("[")) // its a list
type =(List<>).MakeGenericType(type);
return JsonConvert.DeserializeObject(dynamicType.JsonString, type);
}
And here is how it work
var item = new DynamicType(){
TypeName = "DynamicTypeDemo.Cat, DynamicTypeDemo", // or typeof(Cat).AssemblyQualifiedName
JsonString = "{CatName:'Test'}"; // And for a list "[{CatName:'Test'}]"
}
object dynamicObject= ToObject(item); // return it to the javascript
Cat cat = dynamicObject as Cat; // Cast it if you want
I'm trying to create a .NET class that will be exposed to JavaScript via the Jurassic JavaScript engine. The class represents an HTTP response object. Since an HTTP response may have multiple headers with the same name, I would like to include a method that returns an IEnumerable of the headers with a particular name.
This is what I have so far:
public class JsResponseInstance : ObjectInstance
{
private IDictionary<string, IList<string>> _headers;
public JsResponseInstance(ObjectInstance prototype)
: base(prototype)
{
this.PopulateFunctions();
_headers = new Dictionary<string, IList<string>>();
}
[JSFunction(Name = "addHeader")]
public virtual void addHeader(string name, string value)
{
IList<string> vals;
bool exists = _headers.TryGetValue(name, out vals);
if (!exists)
{
vals = new List<string>();
_headers[name] = vals;
}
vals.Add(value);
}
[JSFunction(Name = "getHeaders")]
public virtual IList<string> getHeaders(string name)
{
IList<string> vals;
bool exists = _headers.TryGetValue(name, out vals);
if (!exists)
{
return new List<string>();
}
return vals;
}
}
When I test the getHeaders method I get a JavascriptException: Unsupported type: System.Collections.Generic.IList'1[System.String]
I've tried changing the return type of the getHeaders method from IList to string[] and also adding the optional IsEnumerable property to the JSFunction attribute decorating the method. Neither change made a difference, I was still seeing the same exception.
Is there any way of returning an IEnumerable from a method in a .NET class that is exposed to JavaScript?
Paul Bartrum, the maintainer of Jurassic, answered this question on GitHub.
He stated that the method has to return a type derived from ObjectInstance. Since we need an enumerable, that return type should be an ArrayInstance.
The final working .NET code is:
[JSFunction(Name = "getHeaders")]
public virtual ArrayInstance getHeaders(string name)
{
IList<string> vals;
bool exists = _headers.TryGetValue(name, out vals);
if (!exists)
{
return this.Engine.Array.New();
}
return this.Engine.Array.New(vals.ToArray());
}
I have the function on my view:
function setMessengerState() {
var serviceURL = $("#messenger-set-state-url").val();
var data = {
messengerState: g_messengerState,
};
$.ajax({
type: "POST",
url: serviceURL,
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
success: successFunc,
error: errorFunc
});
function successFunc(data, status) {
console.log("saved");
}
function errorFunc(data, status) {
console.log("failed?");
}
}
This is how the JSON.stringify(data) is formated on Chrome debugger:
"{"messengerState":{"IsOpen":true,"ConversationStates":[{"PartnerId":"64c71990-9ddc-4967-8821-a8e5936560a3","IsEnabled":true},{"PartnerId":"64c71990-9ddc-4967-8821-a8e5936560a3","IsEnabled":true}]}}"
And this is the value of $("#messenger-set-state-url").val():
"/Messenger/Messenger/SetMessengerState"
The controller method:
[HttpPost]
public async Task<ActionResult> SetMessengerState(MessengerStateInfo messengerState)
{
var user = User.ApplicationUser();
if (user == null)
return null;
bool success = await MvcApplication.Messenger.SetState(user, messengerState) != null;
return Json(success, JsonRequestBehavior.DenyGet);
}
And, finally, this is MessengerStateInfo and ConversationStateInfo:
public class MessengerStateInfo
{
public bool IsOpen { get; set; }
public ICollection<ConversationStateInfo> ConversationStates { get; set; }
public MessengerStateInfo()
{
ConversationStates = new ConversationStateInfo[0];
}
}
public class ConversationStateInfo
{
public string PartnerId { get; set; }
public bool IsEnabled { get; set; }
}
I cannot find where I'm doing it wrong. The post never gets to the controller method. I tried before with basic (string) parameters and it works just fine, but it simply doesn't get through with complex objects.
Thank you for writing a detailed question that allows reproducing the problem.
The model binder expects your ICollection to be writable, but arrays are not writable in this manner. Do a simple experiment:
ICollection<int> a = new int[0];
a.Clear();
A "Collection is read-only" exception will be thrown.
Now, how do you fix this. Change your MessengerStateInfo class definition to the following:
public class MessengerStateInfo
{
public bool IsOpen { get; set; }
public ICollection<ConversationStateInfo> ConversationStates { get; set; }
}
Here we removed the constructor, which allows the model binder to create a new instance of List<> type. This one, of course will be read-write and the binding succeeds.
Here are relevant code snippets from the model binder source code. This one is from System.Web.ModelBinding.CollectionModelBinder<TElement> class:
protected virtual bool CreateOrReplaceCollection(ModelBindingExecutionContext modelBindingExecutionContext, ModelBindingContext bindingContext, IList<TElement> newCollection)
{
CollectionModelBinderUtil.CreateOrReplaceCollection<TElement>(bindingContext, newCollection, () => new List<TElement>());
return true;
}
And this one is one is from System.Web.ModelBinding.CollectionModelBinderUtil:
public static void CreateOrReplaceCollection<TElement>(ModelBindingContext bindingContext, IEnumerable<TElement> incomingElements, Func<ICollection<TElement>> creator)
{
ICollection<TElement> model = bindingContext.Model as ICollection<TElement>;
if ((model == null) || model.IsReadOnly)
{
model = creator();
bindingContext.Model = model;
}
model.Clear();
foreach (TElement local in incomingElements)
{
model.Add(local);
}
}
As you can clearly see from this code, if the collection not empty Clear method is called on it, which, in your case leads to an exception. If, on the other hand the collection is null, new List<> is executed which results in a brand new (writable) object.
Note: implementation details may differ depending on software version. The code above may differ from the actual code in the version of the library that you are using. The principle remains the same though.
Here is a tip how to find the reason faster than typing the question to Stackoverflow.
Put a breakpoint on public async Task<ActionResult> SetMessengerState(MessengerStateInfo messengerState) and observe that the breakpoint is not hit. Open chrome console and take notion of the error icon.
Click on the icon to see the error message at the bottom.
Now click on then link in the error message, you'll see this screen:
Finally click on the request in the "Name" column. You will see this:
This gets you the actual error message with the stack trace. In most cases you will be able to tell what's wrong from this message. In this particular case you will immediately see that an array is tried to be cleared and fails.
The error is actually modifying ICollection<ConversationStateInfo> object in ConversationStates = new ConversationStateInfo[0];
Changing it to List should solve the issue.
Set $.ajaxSettings.traditional to true. I already have the same problem, and works for me
$.ajaxSettings.traditional = true;
$('#btn').click(function () {
var array = [];
var url = '/Controller/Action';
$.post(url, { array : array });
});
});
So I have an a tag with an onclick:
<a onclick="join(4);">Join</a>
Now when the a tag is clicked, it calls this code in this order:
JavaScript function:
function join(gymID) {
PageMethods.getGymInformation(gymID);
}
C# method:
[WebMethod]
public gymData getGymInformation(string gymID)
{
gyms gym = new gyms();
DataTable dt = gym.getNewGymInfo(System.Convert.ToInt32(gymID));
DataRow dr = dt.Rows[0];
return new gymData { name = dr["name"].ToString(), strength = dr["strength"].ToString(), speed = dr["speed"].ToString(), defence = dr["defence"].ToString()};
}
public DataTable getNewGymInfo(int gymID)
{
// This returns a datatable with 1 row
return gymTableApapter.getNewGymInfo(gymID);
}
[Serializable]
public sealed class gymData
{
public string name { get; set; }
public string strength { get; set; }
public string speed { get; set; }
public string defence { get; set; }
}
As you can see, the JavaScript join function calls the C# method which then retrieves a DataTable with 1 row, then using a custom data type it populates the strings with data to be returned..
Now I'm trying to figure out how to get the information returned from the C# method to be extracted in the JavaScript join function?
Is their a way to do this?
Add success/error callbacks in your JavaScript code. It might look something like this:
PageMethods.getGymInformation(gymID, onSuccess, onError);
function onSuccess (result) {
// result contains the returned value of the method
}
function onError (result) {
// result contains information about the error
}
Then in the onSuccess function you would have the returned value of the server-side method (of type gymData) in the result variable. At that point you can do whatever you need to do with it in your client-side code.
If the functions don't have any applicable re-use, you can even just add them in-line:
PageMethods.getGymInformation(gymID,
function (result) {
// success
}, function (result) {
// error
});
I am using angularjs in asp.net
I made a controller with CRUD and am trying to get data from angularjs controller using $http service
Route params is getting correct querys from url, i tested that, but i get undefined error when requesting data
What am i doing wrong? :(
SongsController.cs method:
public ActionResult Index(string id)
{
/*var result = db.Songs.ToList();
return Json(result, JsonRequestBehavior.AllowGet);*/
string searchString = id;
var songs = from m in db.Songs
select m;
if (!String.IsNullOrEmpty(searchString))
{
songs = songs.Where(s => s.Title.Contains(searchString));
}
return Json(songs, JsonRequestBehavior.AllowGet);
}
songsService.js:
myApp.factory('songsService', ['$http', function ($http) {
var songsService = {};
songsService.getSongs = function (param) {
return $http.get('/Songs/Index/' + param);
}
return songsService;}])
songsController.js:
myApp.controller('songsController', ['$scope', '$routeParams', 'songsService', function ($scope, $routeParams, songsService) {
var search = $routeParams.query;
if (search == 'undefined' || search == null)
search = '';
getSongs(search);
function getSongs(searchText) {
songsService.getSongs(searchText)
.success(function (data) {
$scope.songs = data;
})
.error(function (error) {
$scope.status = 'Unable to load data: ' + error.message;
console.log($scope.status);
});
}}]);
EDIT:
Song class:
using System;
using System.Collections.Generic;
public class Song
{
public int ID { get; set; }
public string Title { get; set; }
public string Artist { get; set; }
public virtual ICollection<Navigation> Navigations { get; set; }
}
EDIT2: Navigation class:
using System;
public class Navigation
{
public int ID { get; set; }
public int SongID { get; set; }
public int PlaylistID { get; set; }
public virtual Song Song { get; set; }
public virtual Playlist Playlist { get; set; }
}
EDIT3:
If I name my .cs controller SongsController and navigate to url songs/index/something i get popup if i want to open or save something.json and just get redirected back to my default url defined by ngroute (#/songs/)
But, if i name .cs controller something else, like RandomController, if i navigate to same url i get this error:
A circular reference was detected while serializing an object of type 'System.Data.Entity.DynamicProxies.Navigation_7A1A3B789B740F23BAB0A6DAABE519BE3AF91C300893047C23FF2FD8C44E6705'.
EDIT4: I've come to point at which everything if my SongsController.cs looks like this:
public ActionResult Index(string id)
{
var song = new List<Song>
{
new Song{Title="Paint It Black",Artist="Rolling Stones"},
new Song{Title="People Are Strange",Artist="The Doors"},
new Song{Title="With Or Without You",Artist="U2"},
new Song{Title="Wish You Were Here",Artist="Pink Floyd"},
new Song{Title="Fluorescent Adolescent",Artist="Arctic Monkeys"},
new Song{Title="La Guitarra",Artist="Orjan Nilsen"},
new Song{Title="Ping Pong",Artist="Armin Van Buuren"},
new Song{Title="Fade Out Lines",Artist="The Avenger"},
new Song{Title="Redemption Song",Artist="Bob Marley"},
new Song{Title="Wherever I May Roam",Artist="Metallica"},
};
return Json(songs, JsonRequestBehavior.AllowGet);*/
}
If it' like that everything works, but if it looks like i've wrote in original post i get undefined error when i run $http.get :/
EDIT5: Okay, I believe the problem is i'm trying to send objects containing array of Navigation class objects, how can i solve this? :(
You have a circular reference on your Song class.
When the Json serializer tries to process it, it finds the Navigations property and tries to serialize that as well, the problem is that each Navigation object on that collection have a instance of the same Song, so it enters a infinite loop trying to serialize all of it over and over again.
That happens because EntityFramework has its lazyloading and automatically populate the classes as the serializer tries to access them.
To fix it, you can do two things, simply disable the lazyloading for that call in particular:
public ActionResult Index(string id)
{
db.Configuration.LazyLoadingEnabled = false;
string searchString = id;
var songs = from m in db.Songs
select m;
if (!String.IsNullOrEmpty(searchString))
{
songs = songs.Where(s => s.Title.Contains(searchString));
}
return Json(songs, JsonRequestBehavior.AllowGet);
}
The other option is to create a model with only the data you need to return and populate it manually (or using a mapper tool).
public ActionResult Index(string id)
{
db.Configuration.LazyLoadingEnabled = false;
string searchString = id;
var songs = from m in db.Songs
select m;
if (!String.IsNullOrEmpty(searchString))
{
songs = songs.Where(s => s.Title.Contains(searchString));
}
var mappedSongs = songs.Select(it => new { Title = it.Title, Artist = it.Artist }).ToList();
return Json(mappedSongs , JsonRequestBehavior.AllowGet);
}