Is it possible to have a synchronised version of the Blazor Syncfusion confirm and prompt dialog?
Meaning that a modal dialog is shown from a method, awaits the yes/no confirmation, returns the users input and continues the method that initially showed the dialog?
We have validated the reported query. Yes, you can perform you actions based on the SfDialog button clicked, using the lambda expression. Check the below code block for reference.
#using Syncfusion.Blazor.Popups
#using Syncfusion.Blazor.Buttons
<SfButton #onclick="#OpenDialog">Open Dialog</SfButton>
<SfDialog Width="250px" ShowCloseIcon="true" IsModal="true" #bind-Visible="#IsVisible">
<DialogTemplates>
<Header> Dialog </Header>
<Content> This is a Dialog with button and primary button </Content>
</DialogTemplates>
<DialogButtons>
<DialogButton Content="Confirm" IsPrimary="true" OnClick='(e => CloseDialog("Yes"))' />
<DialogButton Content="Cancel" OnClick='(e => CloseDialog("No"))' />
</DialogButtons>
</SfDialog>
#code {
private bool IsVisible { get; set; } = true;
private void OpenDialog()
{
this.IsVisible = true;
}
private void CloseDialog(string userResponse)
{
if (userResponse == "Yes")
{
// Perform your action
this.IsVisible = false;
} else
{
this.IsVisible = true;
}
}
}
Please let us know if the solution helps,
Thanks NDRA JITH,
You suggestion contains two methods, it would be nice if it's possible in one method, just like the confirm method in JavaScript. So open the Confirm dialog wait for the user to answer and based on the answer continue in the same method.
In Blazored it is possible (but I want it in Syncfusion) with the following code:
Parent screen:
#inject IModalService Modal
<BlazoredModal />
#code {
var modalImport = Modal.Show<ModalImport>("Title comes here");
var result = await modalImport.Result;
if (!result.Cancelled)
{
var doSomethingWithThisResult = result.Data;
}
}
Modal screen:
#inject IModalService ModalService;
<button #onclick="HandleStartImport" class="btn btn-secondary">Start</button>
<button #onclick="No" class="btn btn-secondary">Cancel</button>
#code {
[CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; }
async Task HandleStartImport()
{
BlazoredModal.Close(ModalResult.Ok("I clicked OK!"));
}
void No() => BlazoredModal.Cancel();
}
Related
How to do this in Razor pages?
On page load I want to load a list of Cars into an object that I can later access when I load a partialview via an AJAX call.
When I run the code below (after I click the 'Load' button) the values of 'Cars' in OnGetCarPartial is null...
How do I maintain the value of 'Cars' between initial content page load and the subsequent AJAX call?
(Note the reason I want to do this, is that eventually I want _carService.ReadAll to run asynchronously in a thread on page load, updating the 'Cars' list every second, so whenever I load the partialview, I get the latest 'Cars' list without having to call _carService.ReadAll().)
PageModel:
namespace ApiTest.Pages.Cars
{
public class AjaxPartialModel : PageModel
{
private ICarService _carService;
public List<Car> Cars { get; set; }
public AjaxPartialModel(ICarService carService)
{
_carService = carService;
}
public async Task OnGetAsync()
{
if (Cars == null)
{
Cars = _carService.ReadAll();
}
}
public PartialViewResult OnGetCarPartial()
{
return Partial("_CarPartial", Cars);
}
}
}
Content page AJAX
#page
#model AjaxPartialModel
<p><button class="btn btn-primary" id="load">Load</button></p>
<div id="grid"></div>
#section scripts{
<script>
$(function () {
$('#load').on('click', function () {
$('#grid').load('/Cars/Index?handler=CarPartial');
});
});
</script>
}
I have a hosted Blazor WebAssembly application.
I need a strategy or a sample on how can I copy values from an excel spreadsheet and paste them into the application with a final goal to add them into my database through the existing API.
So the question here is this: what components should I paste the values into, and how should I handle the whole process:
excel > clipboard > Component > save in db
It was actually more difficult than I initially thought. I've created a repo. The result is this.
You can select any elements in Excel, copy them, focus the content of your Blazor page and paste it. As a simple view, it is displayed in a table.
Let's go through the solution.
Index.razor
#page "/"
<div class="form-group">
<label for="parser">Parser type</label>
<select class="form-control" id="parser" #bind="_parserType">
<option value="text">Text</option>
<option value="html">HTML</option>
</select>
</div>
<PasteAwareComponent OnContentPasted="FillTable">
#if (_excelContent.Any() == false)
{
<p>No Content</p>
}
else
{
<table class="table table-striped">
#foreach (var row in _excelContent)
{
<tr>
#foreach (var cell in row)
{
<td>#cell</td>
}
</tr>
}
</table>
}
</PasteAwareComponent>
<button type="button" class="btn btn-primary" #onclick="#( () => _excelContent = new List<String[]>() )">Clear</button>
#code
{
private IList<String[]> _excelContent = new List<String[]>();
...more content, explained later...
}
If you copy a selection from Excel into the clipboard, not a single text is copied, but multiple representations of the same content. In my experiment, it has been three different types.
I've built two different parser: ExcelHtmlContentParser and ExcelTextContentParser. Regarding the many different possibilities of what a cell content in Excel can be, my implementation is merely completed and should be seen as an inspiration. To see both parsers in action, you can choose between them by changing the value in the select box.
The PasteAwareComponent handles the interaction with Javascript. You can place any content inside this component. If this component (or any child) has focus, the paste event will be handled correctly.
<span #ref="_reference">
#ChildContent
</span>
#code {
private ElementReference _reference;
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public EventCallback<IEnumerable<IDictionary<String, String>>> OnContentPasted { get; set; }
[JSInvokable("Pasted")]
public async void raisePasteEvent(IEnumerable<IDictionary<String, String>> items)
{
await OnContentPasted.InvokeAsync(items);
}
}
The component handles the interoperation with javascript. As soon the paste events happen the EventCallback<IEnumerable<IDictionary<String, String>>> OnContentPasted is fired.
Potentially, there could be more than one element inside the clipboard. Hence, we need to handle a collection IEnumerable<>. As seen in the picture before, the same clipboard item can have multiple representations. Each representation has a mime-type like "text/plain" or "text/html" and the value. This is represented by the IDictionary<String, String> where the key is the mime-type, and the value is the content.
Before going into the details about the javascript interop, we go back to the Index component.
<PasteAwareComponent OnContentPasted="FillTable">
...
</PasteAwareComponent>
#code {
private async Task FillTable(IEnumerable<IDictionary<String, String>> content)
{
if (content == null || content.Count() != 1)
{
return;
}
var clipboardContent = content.ElementAt(0);
IExcelContentParser parser = null;
switch (_parserType)
{
case "text":
parser = new ExcelTextContentParser();
break;
case "html":
parser = new ExcelHtmlContentParser();
break;
default:
break;
}
foreach (var item in clipboardContent)
{
if (parser.CanParse(item.Key) == false)
{
continue;
}
_excelContent = await parser.GetRows(item.Value);
}
}
}
The index component uses this event callback in the method FillTable. The method checks if there is one element in the clipboard. Based on the selection, the parser is chosen. Each representation is checked in the next step if the chosen parser can parse it, based on the provided mime-type. If the right parser is found, the parser does its magic, and the content of the field _excelContent is updated. Because it is an EventCallback StateHasChanged is called internally, and the view is updated.
The text parser
In the text representation, Excel uses \r\n as the end of the row and a \t for each cell, even the empty ones. The parser logic is quite simple.
public class ExcelTextContentParser : IExcelContentParser
{
public String ValidMimeType { get; } = "text/plain";
public Task<IList<String[]>> GetRows(String input) =>
Task.FromResult<IList<String[]>>(input.Split("\r\n", StringSplitOptions.RemoveEmptyEntries).Select(x =>
x.Split("\t").Select(y => y ?? String.Empty).ToArray()
).ToList());
}
I haven't tested how this behavior changes if the content is more complex. I guess that the HTML representation is more stable. Hence, the second parser.
The HTML parser
The HTML representation is a table. With <tr> and <td>. I've used the library AngleSharp as HTML parser.
public class ExcelHtmlContentParser : IExcelContentParser
{
public String ValidMimeType { get; } = "text/html";
public async Task<IList<String[]>> GetRows(String input)
{
var context = BrowsingContext.New(Configuration.Default);
var document = await context.OpenAsync(reg => reg.Content(input));
var element = document.QuerySelector<IHtmlTableElement>("table");
var result = element.Rows.Select(x => x.Cells.Select(y => y.TextContent).ToArray()).ToList();
return result;
}
}
We are loading the clipboard content as an HTML document, getting the table and iterating over all rows, and selected each column.
** The js interop ***
#inject IJSRuntime runtime
#implements IDisposable
<span #ref="_reference">
#ChildContent
</span>
#code {
private ElementReference _reference;
private DotNetObjectReference<PasteAwareComponent> _objectReference;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender == true)
{
_objectReference = DotNetObjectReference.Create(this);
await runtime.InvokeVoidAsync("BlazorClipboadInterop.ListeningForPasteEvents", new Object[] { _reference, _objectReference });
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
if (_objectReference != null)
{
_objectReference.Dispose();
}
}
}
The PasteAwareComponent component overrides the OnAfterRenderAsync life cycle, to invoke a js interop method. It has to be the OnAfterRenderAsync because before, the HTML reference wouldn't exist, and we need the reference to add the paste event listener. When the paste event occurred the javascript has to call this object, so we need to create a DotNetObjectReference instance. We implemented the IDisposable interface and disposing the reference correctly to prevent memory leaks.
The last part is the javascript part itself. I've created a file called clipboard-interop.js and placed it inside the wwwroot/js folder.
var BlazorClipboadInterop = BlazorClipboadInterop || {};
BlazorClipboadInterop.ListeningForPasteEvents = function (element, dotNetObject) {
element.addEventListener('paste', function (e) { BlazorClipboadInterop.pasteEvent(e, dotNetObject) });
};
We use the HTML reference to register an event listener for the 'paste' event. In the handling method, we create the object that is passed to the C# method.
BlazorClipboadInterop.pasteEvent =
async function (e, dotNetObject) {
var data = await navigator.clipboard.read();
var items = []; //is passed to C#
for (let i = 0; i < data.length; i++) {
var item = {};
items.push(item);
for (let j = 0; j < data[i].types.length; j++) {
const type = data[i].types[j];
const blob = await data[i].getType(type);
if (blob) {
if (type.startsWith("text") == true) {
const content = await blob.text();
item[type] = content;
}
else {
item[type] = await BlazorClipboadInterop.toBase64(blob);
}
}
}
}
dotNetObject.invokeMethodAsync('Pasted', items);
e.preventDefault();
}
When we are using js interop, we should use objects that are easy to serialize. In the case of a real blob, like an image, it would be based64-encoded string, otherwise just the content.
The solution used the navigator.clipboard capabilities. The user needs to allow it. Hence we see the dialog.
I need some help/advise on how to make this work.
I need to pass the model from the view to the controller through an ActionLink
#Html.ActionLink("Radera", "DeleteTraffic", new { model = Model, trafficId = traffic.Id }, new { #class = "btn btn-link NoBorder NoBackGround" })
the method in the controller looks like this.
public ActionResult DeleteTraffic(CalendarModel model, int trafficId)
{
return View("EditDay", model);
}
I have not put any code in the method yet, I've only been debugging it to get the call to work. model is null when I press the button, trafficId is however correctly set. so what have I done wrong?
Edit 1:
I've changed the code according to the suggestions here.
#using (Html.BeginForm("DeleteTraffic", "Calendar", new {trafficId = traffic.Id})) {<input type="submit" value="Radera" class="btn btn-link NoBorder NoBackGround"/>}
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("DeleteTraffic")]
public ActionResult DeleteTraffic(int trafficId)
{
return View("EditDay", Model);
}
but DeleteTraffic is never reched, instead it calls the Main Action for this page.
// GET: Calendar
public ActionResult Calendar()
{
CalendarModel model = new CalendarModel {SelectedDate = DateTime.Today};
if (Request.HttpMethod == "POST")
{
if (!string.IsNullOrEmpty(Request.Form.Get("submit.SelectDate")))
{
model.SelectedDate = Convert.ToDateTime(Request.Form["selectedDate"]);
model.TrafficDates = TrafficData.GeTrafficDatesPerMonth(model.SelectedDate);
Model = model;
return View("EditDay", Model);
}
}
Model = model;
return View(Model);
}
should I just tuck the trafficId into a hiddenfield and use this action for everything? MVC seems so inflexible at times...
First, something like a "delete" should never be handled by GET. Deleting is atomic and should be done utilizing either the POST or DELETE (preferably) verbs. Generally, you also should not just delete something without user confirmation, so the simplest and correct way to handle this would be to have the "delete" link take the user to a view that asks them to confirm deleting the item. On this view, then, you would submit the id of the item to be deleted via a form post:
public ActionResult Delete(int id)
{
var foo = db.Foos.Find(id);
if (foo == null)
{
return new HttpNotFoundResult();
}
return View(foo);
}
[HttpPost]
[ValidateAntiForgeryToken]
[ActionName("Delete")]
public ActionResult DeleteConfirm(int id)
{
var foo = db.Foos.Find(id);
if (foo == null)
{
return new HttpNotFoundResult();
}
db.Foos.Remove(foo);
db.SaveChanges();
return RedirectToAction("Index");
}
Then, for your GET action, you would add a Delete.cshtml file:
#model Namespace.To.Foo
<p>Are you sure you want to delete the foo, "#Model.Name"?</p>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.HiddenFor(m => m.Id)
#Html.ActionLink("Cancel", "Index")
<button type="submit">Delete</button>
}
Alternatively (or rather progressively, as you should still have the previous method as a fallback), you could use a JavaScript confirm and AJAX to do this, if you don't want to change pages:
#Html.ActionLink("Radera", "DeleteTraffic", new { id = item.Id }, new { #class = "btn btn-link NoBorder NoBackGround delete", data_id = item.Id })
Then:
<script>
$('.delete').on('click', function () {
var $deleteLink = $(this);
if (confirm('Are you sure?')) {
$.post('/url/for/delete/', { id = $deleteLink.data('id') }, function () {
$deleteLink.closest('tr').remove();
});
}
});
</script>
I'm trying to set up some basic navigation on a web site I'm rewriting and I've run into a brick wall and don't see why this is not working. I'm doing something similar in a half dozen other places but it just ain't working.
What I want to do is if my article has a next and or previous ID I want to show a navigation bar with appropriate forward/reverse navigation arrows or whatever to allow user to navigate pages.
The ViewModel
public class NavViewModel
{
public int NextID { get; set; }
public int PreviousID { get; set; }
public string NextString { get; set; }
public string PreviousString { get; set; }
public bool SelectedMode { get; set; }
public NavViewModel() { }
}
The View
#Html.HiddenFor(model => model.NavigationViewModel.PreviousID)
#Html.HiddenFor(model => model.NavigationViewModel.NextID)
<div class="post-nav">
#if (#Model.NavigationViewModel.PreviousString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { #nvm = Model.NavigationViewModel }))
{
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.PreviousString" />
}
}
#if (#Model.NavigationViewModel.NextString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { nvm = #Model.NavigationViewModel }))
{
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.NextString" />
}
}
</div>
and the Controller
[HttpPost]
public ActionResult SinglePost(NavViewModel nvm)
{
return RedirectToAction("SinglePost", "Article", new { postID = nvm.PreviousID });
}
I've tried passing back the bool, the IDs, the ViewModel and they all come null or containing null values.
I had this code in a PartialView and because it wasn't working I moved it up a level into the calling view and it has the same result.
You have stated you want to navigate to the next and previous items so using forms and inputs and submitting to a POST method is not appropriate. Instead use a link to navigate to a GET method, passing the ID of the previous or next item.
#if (#Model.NavigationViewModel.PreviousString != null)
{
#Html.ActionLink(Model.NavigationViewModel.PreviousString, "SinglePost", "Article", new { postID = Model.NavigationViewModel.PreviousID }, null)
}
#if (#Model.NavigationViewModel.NextString != null)
{
#Html.ActionLink(Model.NavigationViewModel.NextString , "SinglePost", "Article", new { postID = Model.NavigationViewModel.NextID }, null)
}
The reason your code does not work will be obvious when you inspect the html generated for the <form> tag. Your generating an attribute nvm="YourAssembly.NavigationViewModel" (not a route value). If you used the correct overload to generate route values, which would be
using (Html.BeginForm("SinglePost", "Article", new { #nvm = Model.NavigationViewModel }))
it will still fail because it will generate something similar to (depending on you routes) action="/Article/SinglePost?nvm=YourAssembly.NavigationViewModel" so when you post back, the DefaultModelBinder will try to assign the string "YourAssembly.NavigationViewModel" to parameter nvm, but nvm is a complex object, not a string, so binding will fail.
You could make the POST method work by using
using (Html.BeginForm("SinglePost", "Article", Model.NavigationViewModel))
however this is just degrading performance by posting back unnecessary data and if your model contained properties that were complex objects or collections, it would fail anyway, so don't do it.
Finally, if you want to make the link look like a button, then style it using css.
Try to move hidden inputs into the form
<div class="post-nav">
#if (#Model.NavigationViewModel.PreviousString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { #nvm = Model.NavigationViewModel }))
{
#Html.HiddenFor(model => model.NavigationViewModel.PreviousID)
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.PreviousString" />
}
}
#if (#Model.NavigationViewModel.NextString != null)
{
using (Html.BeginForm("SinglePost", "Article", FormMethod.Post, new { nvm = #Model.NavigationViewModel }))
{
#Html.HiddenFor(model => model.NavigationViewModel.NextID)
<input type="submit" class="btn btn-default" value="#Model.NavigationViewModel.NextString" />
}
}
</div>
I want to take a view and instead of opening a new page I want to just open that view inside a Jquery dialog. I was just wondering how it's done or if possible.
HomeController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Jquery_Dialog.Models;
using Kendo.Mvc.Extensions;
using Kendo.Mvc.UI;
namespace Jquery_Dialog.Controllers
{
public class HomeController : Controller
{
private IEnumerable<Product> Products
{
get
{
return new List<Product>
{
new Product {ProductID = 1, Name = "Train", Category = "Toy", Price = 29.99M},
new Product {ProductID = 2, Name = "Truck", Category = "Toy", Price = 19.99M},
new Product {ProductID = 3, Name = "Bread", Category = "Food", Price = 2.49M},
new Product {ProductID = 4, Name = "Cookies", Category = "Food", Price = 2.99M}
};
}
}
public ActionResult Index()
{
IEnumerable<Product> productList = Products;
return View(productList);
}
public ActionResult Details(int id)
{
Product model = Products.Where(p => p.ProductID == id).SingleOrDefault();
return Request.IsAjaxRequest() ? PartialView(model) : PartialView(model);
}
public ActionResult About()
{
ViewBag.Message = "Your app description page.";
return View();
}
public ActionResult Contact()
{
ViewBag.Message = "Your contact page.";
return View();
}
}
}
Index.cshtml
#model IEnumerable<Jquery_Dialog.Models.Product>
<link rel="stylesheet" href="http://code.jquery.com/ui/1.9.1/themes/base/jquery-ui.css " />
<script src="http://code.jquery.com/jquery-1.8.2.js "></script>
<script src="http://code.jquery.com/ui/1.9.1/jquery-ui.js "></script>
<table> #foreach (var item in Model) {
<tr>
<td>
#Html.ActionLink(item.Name, "Details", new { id = item.ProductID }, new { #class = "ajax-details" })
</td>
</tr>
}
</table>
<div id="dialog" title="Title of dialog">
<p>This is the default dialog which is useful for displaying information. The dialog window can be moved, resized and closed with the 'x' icon.</p>
</div>
<script>
$(function () {
$('.ajax-details').on('click', function (e) { // bind to click event
// go get the page using the link's `href`
$.get($(this).prop('href'), function (response) {
$(response).dialog(); // take the response and throw it up in a dialog
// optional: Use jQuery UI's options to specify buttons and other
// settings here as well. It's also probably a good idea to
// bind the close event and $().remove() this element from
// the page on close since the user can click links over and
// over. (prevent DOM overload of hidden elements)
});
e.preventDefault(); // don't let it continue on
});
});
</script>
<script>
$("#dialog").dialog();
</script>
As you can see I have a simple dialog that opens a div but I want to be able to open the details view instead of clicking the ActionLink and going to a different page, I want to be able to click the ActionLink and have it open up in the dialog.
Assuming you make the ActionLink a little more accessible (by using a class name for instance):
#Html.ActionLink(item.Name, "Details", new { id = item.ProductID },
/* htmlAttributes: */ new { #class = "ajax-details" })
You also make a modification to the action so we can fetch partial contents when it's an ajax request:
public ActionResult Details(int id)
{
// this is another way of making sure that AJAX calls get partial content,
// but a normal visit would render the entire page.
return Request.IsAjaxRequest() ? PartialView(model) : View(model);
}
Optional You could also adjust your _ViewStart.cshtml file to do the same if this was common place on the website to render partial views/ajax supplementing:
#{
Layout = IsAjax ? null : "~/Views/Shared/_Layout.cshtml";
}
Now, we wire it up with AJAX. Again, reference the class name we game the link earlier (ajax-details):
$('.ajax-details').on('click',function(e){ // bind to click event
// go get the page using the link's `href`
$.get($(this).prop('href'), function(response){
$(response).dialog(); // take the response and throw it up in a dialog
// optional: Use jQuery UI's options to specify buttons and other
// settings here as well. It's also probably a good idea to
// bind the close event and $().remove() this element from
// the page on close since the user can click links over and
// over. (prevent DOM overload of hidden elements)
});
e.preventDefault(); // don't let it continue on
});
Don't have the opportunity to test it, but should get you in the ball park. if it doesn't get you close enough, let me know and I'll revisit the answer and adjust.