Trying to understand HttpPost in MVC3 [duplicate] - c#

This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
MVC(3) handleUpdate
I'm (slowly) learning how to use MVC 3 and at the moment I'm having a looking at the MvcMusicStore tutorial app on the asp.net website.
Right now I'm trying to understand how HttpPost works. From what I can gather, the user performs whatever actions they want in their browser and then with the use of jQuery, the data is posted back to the server (to the corresponding function with [HttpPost] attribute) and then in this case, a json result is sent back to the browser which handles this and updates elements accordingly.
I understand this fine, but in the particular snippet of code I'm looking at, I can't understand how the 'handleUpdate()' function is being hit when there appear to be no calls made from either the js or the server-side code. Is there something I'm missing here? Anyway here is the front-end:
#model MvcMusicStore.ViewModels.ShoppingCartViewModel
#{
ViewBag.Title = "Shopping Cart";
}
<script src="/Scripts/jquery-1.4.4.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
// Document.ready -> link up remove event handler
$(".RemoveLink").click(function () {
// Get the id from the link
var recordToDelete = $(this).attr("data-id");
if (recordToDelete != '') {
// Perform the ajax post
$.post("/ShoppingCart/RemoveFromCart", { "id": recordToDelete },
function (data) {
// Successful requests get here
// Update the page elements
if (data.ItemCount == 0) {
$('#row-' + data.DeleteId).fadeOut('slow');
} else {
$('#item-count-' + data.DeleteId).text(data.ItemCount);
}
$('#cart-total').text(data.CartTotal);
$('#update-message').text(data.Message);
$('#cart-status').text('Cart (' + data.CartCount + ')');
});
}
});
});
function handleUpdate() {
// Load and deserialize the returned JSON data
var json = context.get_data();
var data = Sys.Serialization.JavaScriptSerializer.deserialize(json);
// Update the page elements
if (data.ItemCount == 0) {
$('#row-' + data.DeleteId).fadeOut('slow');
} else {
$('#item-count-' + data.DeleteId).text(data.ItemCount);
}
$('#cart-total').text(data.CartTotal);
$('#update-message').text(data.Message);
$('#cart-status').text('Cart (' + data.CartCount + ')');
}
</script>
<h3>
<em>Review</em> your cart:
</h3>
<p class="button">
#Html.ActionLink("Checkout >>", "AddressAndPayment", "Checkout")
</p>
<div id="update-message">
</div>
<table>
<tr>
<th>
Album Name
</th>
<th>
Price (each)
</th>
<th>
Quantity
</th>
<th></th>
</tr>
#foreach (var item in Model.CartItems)
{
<tr id="row-#item.RecordId">
<td>
#Html.ActionLink(item.Album.Title, "Details", "Store", new { id = item.AlbumId }, null)
</td>
<td>
#item.Album.Price
</td>
<td id="item-count-#item.RecordId">
#item.Count
</td>
<td>
Remove from cart
</td>
</tr>
}
<tr>
<td>
Total
</td>
<td>
</td>
<td>
</td>
<td id="cart-total">
#Model.CartTotal
</td>
</tr>
</table>
and here is the (relevant) server-side code:
//
// AJAX: /ShoppingCart/RemoveFromCart/5
[HttpPost]
public ActionResult RemoveFromCart(int id)
{
// Remove the item from the cart
var cart = ShoppingCart.GetCart(this.HttpContext);
// Get the name of the album to display confirmation
string albumName = storeDB.Carts
.Single(item => item.RecordId == id).Album.Title;
// Remove from cart
int itemCount = cart.RemoveFromCart(id);
// Display the confirmation message
var results = new ShoppingCartRemoveViewModel
{
Message = Server.HtmlEncode(albumName) +
" has been removed from your shopping cart.",
CartTotal = cart.GetTotal(),
CartCount = cart.GetCount(),
ItemCount = itemCount,
DeleteId = id
};
return Json(results);
}
I can see that the handleUpdate() manipulates the DOM based on the returned JSON, but I can't figure out for the life of me how it's being called? Is there some jQuery magic going on or have I completely misunderstood how this all works?
Thanks!

It's not being called.
The relevant code on the client side that calls the RemoveFromCart method on the server side is this:
if (recordToDelete != '') {
// Perform the ajax post
$.post("/ShoppingCart/RemoveFromCart", { "id": recordToDelete },
function (data) {
// Handle result.
});
}
Note the URL is /ShoppingCart/RemoveFromCart, which maps to the URL route for the RemoveFromCart method.
The jQuery post method is being used to make the call to the method on the controller, and then a closure (indicated by the function() { ... }) is passed, not the handleUpdate method.

Related

MVC 5 WEB API post

I am following this tutorial: https://www.codeproject.com/Articles/424461/Implementing-Consuming-ASP-NET-WEB-API-from-JQuery
to help me implement my web API. But I have something different. This is my index:
#model IEnumerable<dsr_vaja1.Models.Kosarica.Kosarica>
#{
ViewBag.Title = "kosarica";
Layout = "~/Views/Shared/MasterStran.cshtml";
}
<div id="accordion">
<h3>Igre</h3>
<div>
<table class="table table-hover ">
<tr>
<th>
#Html.DisplayNameFor(model => model.ime_igre)
</th>
<th>
#Html.DisplayNameFor(model => model.cena_igre)
</th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.DisplayFor(modelItem => item.ime_igre)
</td>
<td>
#Html.DisplayFor(modelItem => item.cena_igre)
</td>
</tr>
}
</table>
<button onclick="AddEmployee();return false;">Add Employee</button>
</div>
</div>
As you can see, here I have a LIST of items named Kosarica. This is my web api:
public void Post(Nakup nakup)
{
nakup.Id = 1;
nc.Nakupi.Add(nakup);
nc.SaveChanges();
}
My web api and everything works completely fine, now I would just like to know how I would use this function:
function AddGame() {
jQuery.support.cors = true;
var game = {
ID: model.id,
ime_igre: model.ime_igre,
};
$.ajax({
url: 'http://localhost:8080/API_SVC/api/EmployeeAPI',
type: 'POST',
data:JSON.stringify(employee),
contentType: "application/json;charset=utf-8",
success: function (data) {
WriteResponse(data);
},
error: function (x, y, z) {
alert(x + '\n' + y + '\n' + z);
}
});
}
FOR ALL of the items in the list after the user clicks a button.
**edit
Isn't there a simpler way to do this? I just realized that the first line of code is a list
#model IEnumerable
model is a list of objects. As you can see below in the foreach loop, I loop through the items in model. So can I just pass the model to the jquery function somehow?
Create a javascript function, eg :
function AddGamesForAllItems(){
// Select the table through JQuery Selector Here
//loop through the elements of the table Here
//Call the AddGame function passing by paremeters the needed elements from the table... You have to declare parameters inside you AddGame function
}
Call the AddGamesForAllItems function when you need it eg: Button click event.
Hope it could help !

MVC PayPal intergration

I have my site working so a user can pay via card however now I also need to use PayPal, I can't seem to send the products over from the shopping cart to the paypal controller, each individual product needs to be sent over in the order.
Here is my PayPal controller;
namespace T_shirt_Company_v3.Controllers
{
public class PayPalController : Controller
{
public ActionResult RedirectFromPaypal()
{
return View();
}
public ActionResult CancelFromPaypal()
{
return View();
}
public ActionResult NotifyFromPaypal()
{
return View();
}
public ActionResult ValidateCommand(string RecordId, string CartTotal)
{
bool useSandbox = Convert.ToBoolean(ConfigurationManager.AppSettings["IsSandbox"]);
var paypal = new PayPal(useSandbox);
paypal.item_name = RecordId;
paypal.amount = CartTotal;
return View(paypal);
}
}
}
And my View for checkout that I need the details from;
#model T_shirt_Company_v3.ViewModels.ShoppingCartViewModel
#{
ViewBag.Title = "Shopping Cart";
}
<script src="/Scripts/jquery-1.4.4.min.js"
type="text/javascript"></script>
<script type="text/javascript">
$(function () {
// Document.ready -> link up remove event handler
$(".RemoveLink").click(function () {
// Get the id from the link
var recordToDelete = $(this).attr("data-id");
if (recordToDelete != '') {
// Perform the ajax post
$.post("/ShoppingCart/RemoveFromCart", {"id": recordToDelete },
function (data) {
// Successful requests get here
// Update the page elements
if (data.ItemCount == 0) {
$('#row-' + data.DeleteId).fadeOut('slow');
} else {
$('#item-count-' + data.DeleteId).text(data.ItemCount);
}
$('#cart-total').text(data.CartTotal);
$('#update-message').text(data.Message);
$('#cart-status').text('Cart (' + data.CartCount + ')');
});
}
});
});
</script>
<center>
<h3>
Review your cart:
</h3>
<p class="button">
#using (Html.BeginForm("ValidateCommand", "PayPal"))
{
<input type="submit" name="btnConfirm" value="Check Out with Paypal" />
}
#Html.ActionLink((string)ViewBag.CartStatus, (string)ViewBag.Link, (string)ViewBag.Link2)
#Html.ActionLink("Continue Shopping ", "Index", "Store")
</p>
<div id="update-message">
</div>
<table>
<tr>
<th>
Product Name
</th>
<th>
Price (each)
</th>
<th>
Quantity
</th>
<th></th>
</tr>
#foreach (var item in
Model.CartItems)
{
<tr id="row-#item.RecordId">
<td>
#Html.ActionLink(item.Product.Title,
"Details", "Store", new { id = item.ProductId }, null)
</td>
<td>
#item.Product.Price
</td>
<td id="item-count-#item.RecordId">
#item.Count
</td>
<td>
<a href="#" class="RemoveLink"
data-id="#item.RecordId">
Remove
from cart
</a>
</td>
</tr>
}
<tr>
<td>
Total
</td>
<td></td>
<td></td>
<td id="cart-total">
#Model.CartTotal
</td>
</tr>
</table>
</center>
And the controller;
namespace T_shirt_Company_v3.Controllers
{
public class ShoppingCartController : Controller
{
TshirtStoreDB storeDB = new TshirtStoreDB();
//
// GET: /ShoppingCart/
public ActionResult Index()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
// Set up the ViewModel
ShoppingCartViewModel viewModel = new ShoppingCartViewModel
{
CartItems = cart.GetCartItems(),
CartTotal = cart.GetTotal()
};
if (viewModel.CartItems.Any())
{
ViewBag.CartStatus = "Proceed to checkout or ";
ViewBag.Link = "AddressAndPayment";
ViewBag.Link2 = "Checkout";
}
else
{
ViewBag.CartStatus = "Cart is empty please ";
ViewBag.Link = "Index";
ViewBag.Link2 = "Store";
}
// Return the view
return View(viewModel);
}
//
// GET: /Store/AddToCart/5(ID)
public ActionResult AddToCart(int id)
{
// Retrieve the Product from the database
var addedProduct = storeDB.Products
.Single(product => product.ProductId == id);
// Add it to the shopping cart
var cart = ShoppingCart.GetCart(this.HttpContext);
cart.AddToCart(addedProduct);
// Go back to the main store page for more shopping
return RedirectToAction("Index");
}
//
// AJAX: /ShoppingCart/RemoveFromCart/5(ID)
[HttpPost]
public ActionResult RemoveFromCart(int id)
{
// Remove the item from the cart
var cart = ShoppingCart.GetCart(this.HttpContext);
// Get the name of the product to display confirmation
string productName = storeDB.Carts
.Single(item => item.RecordId == id).Product.Title;
// Removes item from cart
int itemCount = cart.RemoveFromCart(id);
// Display the confirmation message saying removed from cart
var results = new ShoppingCartRemoveViewModel
{
Message = Server.HtmlEncode(productName) +
" has been removed from your shopping cart.",
CartTotal = cart.GetTotal(),
CartCount = cart.GetCount(),
ItemCount = itemCount,
DeleteId = id
};
return Json(results);
}
//
// GET: /ShoppingCart/CartSummary
[ChildActionOnly]
public ActionResult CartSummary()
{
var cart = ShoppingCart.GetCart(this.HttpContext);
ViewData["CartCount"] = cart.GetCount();
return PartialView("CartSummary");
}
//test close connection when done
protected override void Dispose(bool disposing)
{
storeDB.Dispose();
}
}
}
Assuming I read your code correctly, you're only rendering the data, you're not sending it (at all).
your form only has a button (it's the only "data" sent)
you need to include the data you rendered as form fields (<input />)
Hth.
As PayPal Api is dependent on a set of operations that must be understood before used, developers should focus on understanding the operations summary, before using the Api, also a sandbox is available for testing before officially using the Api.
You should know how to use the RESTful endpoint structure of PayPal.
You can check the PayPal Api documentation for details on how to integrate your web application with PayPal using REST API Reference.

Selection in the Controller is not matching

I got this error when I am trying to remove the item from the Cart table.
HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /Panier/RemoveFromCart/1. This URL seems to be fine with me. It should branch to the PanierController at RemoveCart. I don't understand why it is not branching.
Thanks
Index.cshtml
#model Tp1WebStore3.ViewModels.ShoppingCartViewModel
#{
ViewBag.Title = "Shopping Cart";
}
<script src="/Scripts/jquery-1.4.4.min.js"
type="text/javascript"></script>
<script type="text/javascript">
$(function () {
// Document.ready -> link up remove event handler
$(".RemoveLink").click(function () {
// Get the id from the link
var recordToDelete = $(this).attr("data-id");
if (recordToDelete != '') {
// Perform the ajax post
$.post("/ShoppingCart/RemoveFromCart", {"id": recordToDelete },
function (data) {
// Successful requests get here
// Update the page elements
if (data.ItemCount == 0) {
$('#row-' + data.DeleteId).fadeOut('slow');
} else {
$('#item-count-' + data.DeleteId).text(data.ItemCount);
}
$('#cart-total').text(data.CartTotal);
$('#update-message').text(data.Message);
$('#cart-status').text('Cart (' + data.CartCount + ')');
});
}
});
});
</script>
<h3>
<em>Details</em> du panier:
</h3>
<p class="button">
#Html.ActionLink("Checkout >>", "AddressAndPayment", "Checkout")
</p>
<div id="update-message">
</div>
<table>
<tr>
<th>
Produit
</th>
<th>
Prix (unitaire)
</th>
<th>
Quantite
</th>
<th></th>
</tr>
#foreach (var item in Model.CartItems)
{
<tr id="row-#item.ProduitId">
<td>
#Html.ActionLink(item.Produit.Description,"Details", "Store", new { id =
item.ProduitId }, null)
</td>
<td>
#item.Produit.Prix
</td>
<td id="item-count-#item.ProduitId">
#item.Quantite
</td>
<td>
#Html.ActionLink("Enlever du panier", "RemoveFromCart", "Panier", new { id =
item.ProduitId }, null)
</td>
</tr>
}
<tr>
<td>
Total
</td>
<td></td>
<td></td>
<td id="cart-total">
#Model.CartTotal
</td>
</tr>
</table>
PanierController.cs
namespace Tp1WebStore3.Controllers
{
public class PanierController : Controller
{
Tp1WebStoreDBEntities dbProduit = new Tp1WebStoreDBEntities();
[HttpPost]
public ActionResult RemoveFromCart(int id)
{
// Remove the item from the cart
var cart = ShoppingCart.GetCart(this.HttpContext);
// Get the name of the product to display confirmation
string produitDescription = dbProduit.Paniers
.Single(item => item.PanierId == id).Produit.Description;
// Remove from cart
int itemCount = cart.RemoveFromCart(id);
// Display the confirmation message
var results = new ShoppingCartRemoveViewModel
{
Message = Server.HtmlEncode(produitDescription) +
" has been removed from your shopping cart.",
CartTotal = cart.GetTotal(),
CartCount = cart.GetCount(),
ItemCount = itemCount,
DeleteId = id
};
return View("Details");
}
Your RemoveFromCart controller action is decorated with the [HttpPost] attribute meaning that it is ONLY accessible by POST verbs. But in your view you seem to have generated some action link to it:
#Html.ActionLink(
"Enlever du panier",
"RemoveFromCart",
"Panier",
new { id = item.ProduitId },
null
)
But as you are well aware, an Html.ActionLink translates into an <a> tag in your markup which obviously is sending a GET request to the server when clicked.
So basically you have 3 possibilities here:
Use an Html.BeginForm instead of an ActionLink to refer to this action which would allow you to send a POST request
Get rid of the [HttpPost] attribute from your RemoveFromCart action
AJAXify the anchor which would allow you to use a POST request.

ASP.NET MVC 3 NOT showing appropriate view, action called using jquery

I have a small problem.
My action is :
public ViewResult SearchForRooms(int HotelDDL)
{
List<Room> roomsInHotel = bl.ReturnRoomsPerHotel(HotelDDL);
return View(roomsInHotel);
}
Here is the jquery that is calling the action:
<script type="text/javascript" language="javascript">
$(document).ready(function () {
$("#HotelDDL").change(function () {
var text = $("#HotelDDL option:selected").text();
var value = $("#HotelDDL option:selected").val();
alert("Selected text=" + text + " Selected value= " + value);
$.post("/Home/SearchForRooms",
{
HotelDDL: $("#HotelDDL option:selected").val()
});
});
});
</script>
And finally, here is the View that should be called:
#model IEnumerable<RoomReservation.Entities.Entities.Room>
#{
ViewBag.Title = "Search";
}
<h2>Result</h2>
<table>
<tr>
<th>
City
</th>
<th>
Hotel
</th>
<th>
Room label
</th>
<th>
Number of beds
</th>
<th>
Price per night
</th>
<th></th>
</tr>
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelitem=>item.Hotel.City.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Hotel.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.Name)
</td>
<td>
#Html.DisplayFor(modelItem => item.NumberOfBeds)
</td>
<td>
#Html.DisplayFor(modelItem => item.PricePerNight)
</td>
</tr>
}
</table>
Everthing is working ok (databse return all rooms correctly) except final view rendering.
I have tried Phil's tool but it doesn't give me any suspicious hints:
RouteDebug.RouteDebugger.RewriteRoutesForTesting(RouteTable.Routes);
So, why is it not showing after jscript send it's post method to SearchForRooms()?
Thank you
P.S. If you need any other piece of code please just say so.
Note that doing an ajax post will not refresh or redirect the page like a classic post would.
The view is not showing because there is no where to place it. You post successfully, and return a view successfully but then do nothing with it. If you want to show the returned view, then you have to append it to the dom somehow. Here is a common method:
<div id="successDiv"></div>
<script type="text/javascript" language="javascript">
$(document).ready(function () {
$("#HotelDDL").change(function () {
var text = $("#HotelDDL option:selected").text();
var value = $("#HotelDDL option:selected").val();
alert("Selected text=" + text + " Selected value= " + value);
$.post("/Home/SearchForRooms",
{
HotelDDL: $("#HotelDDL option:selected").val()
}, function(result){//add callback for success - result is the returned view
$("#successDiv").html(result);//place the view inside of the div
});
});
});
</script>
Comment Response
#TunAntun - You should use a classic post if you want the view to have its own page. There is no way to accomplish this from ajax. You could do this with javascript though
$.post("/Home/SearchForRooms",
{
HotelDDL: $("#HotelDDL option:selected").val()
}, function(result){//add callback for success - result is the returned view
$("#successDiv").html(result);//place the view inside of the div
});
would become
var postForm = document.createElement("form");
postForm.setAttribute("method","post");
postForm.setAttribute("action","/Home/SearchForRooms");
var postVal = document.createElement("input");
postVal.setAttribute("type","hidden");
postVal.setAttribute("name","HotelDDL");
postVal.setAttribute("value",$("#HotelDDL option:selected").val() );
postForm.appendChild(postVal);
document.body.appendChild(postForm);
$(postForm).submit();
This will dynamically issue the post you wanted. Then in your controller it will properly redirect or return a view. It is highly suggested that when returning a view from a [HttpPost] that you use RedirectToAction and then return the view from a [HttpGet] method in order to prevent refresh from reposting old data.

Ajax.BeginForm refresh partial..?

Okay so, I have a form that is posting using ajax...
<% using(Ajax.BeginForm(new AjaxOptions() { OnBegin="onBegin", OnSuccess = "onSuccess", OnFailure="onFailure" })) { %>
On the server side I am passing back from the controller a Json object. Now, when the OnSuccess event fires I can get to the Json object by using "result.get_response().get_object()"...
My question is, I need to be able to refresh a partial on the page with a list of items that are in the Json object...
Thoughts on how I can do this..?
Use jQuery, and spin through the returned JSON object, building whatever you like.
Example:
$.each(json, function(i, item) {
//Add a dinner to the list on the right
$('#dinnerList').append($('<li/>')
.attr("class", "dinnerItem")
.append($('<a/>').attr("href", "/Dinners/Details/" + item.ID)
.html(item.Name)).append("SomeThing"));
});
I think that you will find the below code helpful.
Firstly, create an AJAX form with
RefreshAjaxList: the name ajax action of current controller
string.empty (optional)
AJAX option
id of form (optional)
when click status, we will edit the status call server to update the status
after edit the status, we call submit button to call RefreshAjaxList; the button is display:none
in this example, I have one controller: AjaxController with 2 actions:
public ActionResult UpdateStatus(int contactId, Status contactStatus)
{
ContactRepository repo = new ContactRepository();
repo.UpdateStatus(contactId, contactStatus);
return Json("success:true");
}
[AcceptVerbs(HttpVerbs.Post)]
[ActionName("RefreshAjaxList")]
public ActionResult RefreshContact()
{
ContactRepository repo = new ContactRepository();
IList<Contact> list = repo.List();
return PartialView("AjaxUc/AjaxList", repo.List());
}
JavaScript:
var status = { active: 1, inactive: 0 };
function editStatus(cell, id, active) {
if (active)
cell.innerHTML = "<input type='radio' checked='true' name='active" + id + "' onclick='updateStatus(this, " + id + ", true);'>Active</input>" +
"<input type='radio' name='active" + id + "' onclick='updateStatus(this, " + id + ", false);'>Inactive</input>";
else
cell.innerHTML = "<input type='radio' name='active" + id + "' onclick='updateStatus(this, " + id + ", true);'>Active</input>" +
"<input type='radio' checked='false' name='active" + id + "' onclick='updateStatus(this, " + id + ", false);'>Inactive</input>";
}
function updateStatus(radio, id, active) {
if (radio.checked != active) {
if (confirm("Do you want to change the status of the contract?")) {
if (active)
cStatus = status.active;
else
cStatus = status.inactive;
$.ajax({
url: 'Ajax/UpdateStatus',
dataType: "json",
data: { contactId: id, contactStatus: cStatus },
success: function(html) {
jQuery("#divAjaxList").submit();
},
error: function(request, desc, ex) {
alert(desc);
}
});
}
}
}
Your ASP script:
<% using (Ajax.BeginForm("RefreshAjaxList", "abc", new AjaxOptions() { UpdateTargetId = "divContent" }, new { #id = "divAjaxList" }))
{%>
<div id="divContent">
<table>
<tr>
<th>
</th>
<th>
Id
</th>
<th>
FirstName
</th>
<th>
LastName
</th>
<th>
Phone
</th>
<th>
Email
</th>
<th>
Status
</th>
</tr>
<% foreach (var item in Model)
{ %>
<tr>
<td>
<%= Html.ActionLink("Edit", "Edit", new { id = item.Id })%>
|
<%= Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ })%>
</td>
<td>
<%= Html.Encode(item.Id)%>
</td>
<td>
<%= Html.Encode(item.FirstName)%>
</td>
<td>
<%= Html.Encode(item.LastName)%>
</td>
<td>
<%= Html.Encode(item.Phone)%>
</td>
<td>
<%= Html.Encode(item.Email)%>
</td>
<td ondblclick="editStatus(this,<%=item.Id %>, <%= item.Status.GetHashCode() %>);">
<%= item.Status== Status.Active ? "Active" :"Inactive" %>
</td>
</tr>
<% } %>
</table>
<input type="submit" id="refresh" style="display:none" />
</div>
<% } %>
For more info, mail to pnguyen2#firstlook.com for further discussion.
Hope this can help u.
create the user control for the list of items to be displayed and render it as partial by passing the json data to the UC. This will refresh partially
You can also use JTemplate to render JSON data

Categories

Resources