Can you use IAsyncEnumerable in Razor pages to progressively display markup? - razor-pages

I've been playing around with Blazor and the IAsyncEnumerable feature in C# 8.0. Is it possible to use IAsyncEnumerable and await within Razor Pages to progressively display markup with data?
Example service:
private static readonly string[] games = new[] { "Call of Duty", "Legend of Zelda", "Super Mario 64" };
public async IAsyncEnumerable<string> GetGames()
{
foreach (var game in games)
{
await Task.Delay(1000);
yield return game;
}
}
Example in razor page:
#await foreach(var game in GameService.GetGames())
{
<p>#game</p>
}
This gives error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
Any ideas if this is possible?

Server-side Razor allows what you describe. This video describes the code in this Github repo that shows how to use IAsyncEnumerable by modifying the ForecastService example in server-side Blazor template.
Modifying the service itself is easy, and actually results in cleaner code :
public async IAsyncEnumerable<WeatherForecast> GetForecastAsync(DateTime startDate)
{
var rng = new Random();
for(int i=0;i<5;i++)
{
await Task.Delay(200);
yield return new WeatherForecast
{
Date = startDate.AddDays(i),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
};
}
}
The Blazor page on the other hand is more complicated. It's not just that the loop would have to finish before the HTML was displayed, you can't use await foreach in the page itself because it's not asynchronous. You can only define asynchronous methods in the code block.
What you can do, is enumerate the IAsyncEnumerable and notify the page to refresh itself after each change.
The rendering code itself doesn't need to change :
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
#foreach (var forecast in forecasts)
{
<tr>
<td>#forecast.Date.ToShortDateString()</td>
<td>#forecast.TemperatureC</td>
<td>#forecast.TemperatureF</td>
<td>#forecast.Summary</td>
</tr>
}
</tbody>
</table>
OnInitializedAsync needs to call StateHasChanged() after receiving each item :
List<WeatherForecast> forecasts;
protected override async Task OnInitializedAsync()
{
forecasts =new List<WeatherForecast>();
await foreach(var forecast in ForecastService.GetForecastAsync(DateTime.Now))
{
forecasts.Add(forecast);
this.StateHasChanged();
}
}
In the question's example, incoming games could be stored in a List, leaving the rendering code unchanged :
#foreach(var game in games)
{
<p>#game</p>
}
#code {
List<string> games;
protected override async Task OnInitializedAsync()
{
games =new List<games>();
await foreach(var game in GameService.GetGamesAsync())
{
games.Add(game);
this.StateHasChanged();
}
}
}

You can't write await foreach on .razor template code. But, as workaround, you can write it at #code section:
#if (#gamesUI == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Game</th>
</tr>
</thead>
<tbody>
#foreach (var game in gamesUI) // <--- workaround
{
<tr>
<td>#game</td>
</tr>
}
</tbody>
</table>
}
#code {
List<string> gamesUI; // <--- workaround
protected override async Task OnInitializedAsync()
{
gamesUI = new List<string>();
await foreach(var game in GService.GetgamesAsync() )
{
gamesUI.Add(game);
this.StateHasChanged();
}
}
}
Effect:
Yielding data:
private static readonly string[] games = new[] { "Call of Duty", "Legend of Zelda", "Super Mario 64", "Bag man" };
public async IAsyncEnumerable<string> GetgamesAsync()
{
var rng = new Random();
foreach (var game in games)
{
await Task.Delay(1000);
yield return game;
}
}

Related

Copy From excel spread sheet into blazor app

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.

asp-page-handler helper tag not populating the correct hyperlink

I'm having trouble with asp-page-handler populating the correct link when passing in an route value for an ID and not the handler (so "{int:id}" vs "{handler?}"). (See bottom of image).
I'm expecting something such as:
https://localhost:5001/Emp/Download?id=Matthew.pdf
In my small test app I hardcoded the employee id value into the GetDirectoryReference("E000002/stubs/") and it works fine. (Note that the E000002 is the value that changes dependant upon the logged in person. The value does populate in my OnGetAsync() call so no issue there).
Setup for customer interface GetEmployeePayrollNo:
public async Task<Employee> GetEmployeePayrollNo(int id)
{
return await _context.Employees
.Include(e => e.EmployeePayroll)
.Where(p => p.EmployeeId == id)
.FirstAsync();
}
In this test I'm trying to pass a variable into GetDirectoryReference dependant upon who is logged in.
Not sure what I'm messing up since selecting download or view doesn't even hit on debug mode.
Using Azure File Share to hold documents*
Using Razor pages for folder structure*
Pages
Emp
Paystubs.cshtml
Paystubs.cshtml takes an id route value for the person logged in. In my test application it took a "{handler?}" route value. Not sure if I can use both???
Model
private readonly IConfiguration _configuration;
private readonly ICustomer _customer;
public PaystubsModel(IConfiguration configuration,
ICustomer customer)
{
_configuration = configuration;
_customer = customer;
}
public Employee Employee { get; set; }
public List<AzureFileModel> AzureFileModel { get; private set; } = new List<AzureFileModel>();
public async Task OnGetAsync(int id)
{
Employee = await _customer.GetEmployeePayrollNo(id);
var empPayNo = Employee.EmployeePayroll;
string fileStorageConnection = _configuration.GetValue<string>("FileStorage");
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(fileStorageConnection);
CloudFileShare share = storageAccount.CreateCloudFileClient().GetShareReference("test");
CloudFileDirectory root = share.GetRootDirectoryReference();
CloudFileDirectory dir = root.GetDirectoryReference(empPayNo.EmployeeOnline+"/stubs");
// list all files in the directory
AzureFileModel = await ListSubDir(dir);
}
public static async Task<List<AzureFileModel>> ListSubDir(CloudFileDirectory fileDirectory)
{
// LEFT OUT FOR BREVITY
}
public async Task<IActionResult> OnGetDownload(string fileId)
{
var empPayNo = Employee.EmployeePayroll;
string fileStorageConnection = _configuration.GetValue<string>("FileStorage");
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(fileStorageConnection);
CloudFileShare share = storageAccount.CreateCloudFileClient().GetShareReference("test");
CloudFileDirectory rootDir = share.GetRootDirectoryReference();
CloudFileDirectory dir = rootDir.GetDirectoryReference(empPayNo.EmployeeOnline+"/stubs");
CloudFile file = dir.GetFileReference(fileId);
if (!file.Exists())
{
ModelState.AddModelError(string.Empty, "File not found.");
return Page();
}
else
{
await file.DownloadToStreamAsync(new MemoryStream());
Stream fileStream = await file.OpenReadAsync();
return File(fileStream, file.Properties.ContentType, file.Name);
}
}
Page:
#page "{id:int}" //*******In my test model I was using {handler?} but I need to pass in the employee id to route here to display only the logged in employee. Again, not sure if its possible to use both?
#model NavraePortal.WebApp.Pages.Emp.PaystubsModel
#{
ViewData["Title"] = "Documents";
}
<h1>Pay Stub Copies</h1>
<table class="table table-bordered">
<thead>
<tr>
<th>File Name</th>
<th>File Date</th>
<th>Download</th>
<th>View</th>
</tr>
</thead>
<tbody>
#foreach (var data in Model.AzureFileModel)
{
<tr>
<td>#data.FileName</td>
<td>#data.DateModified</td>
<td>
<a class="btn btn-primary" asp-route-id="#data.FileName" asp-page-handler="Download">Download</a>
</td>
<td>
<a class="btn btn-info" asp-route-id="#data.FileName" asp-page-handler="View">View</a>
</td>
</tr>
}
</tbody>
</table>
Because this id in asp-route-id is not matched with the fileId. In this page, you need to modify it.
<a class="btn btn-primary" asp-route-fileId="#data.FileName" asp-page-handler="Download">Download</a>
Then, this url is updated.

Asp.net MVC 5 passing model object to controller via ActionLink

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>

how to map composite key in CRUD functionality

I need to map based on two keys(comp and part).
#foreach (var item in Model) {
<tr>
<td>
#Html.DisplayFor(modelItem => item.comp)
</td>
<td>
#Html.DisplayFor(modelItem => item.part)
</td>
...........................
<td>
#Html.ActionLink("Edit", "Edit", new { id=item.comp }) |
#Html.ActionLink("Details", "Details", new { id = item.comp }) |
#Html.ActionLink("Delete", "Delete", new { id = item.comp })
</td>
</tr>
}
how to use composite key in Index page and also in controller.
public ActionResult Edit(int? id)
{
DataView record = db.RecordDataView.Find(id);
if (record == null)
{
return HttpNotFound();
}
return View(record);
}
If anyone have idea please reply.
The find method, finds entities by the primary key. If you have a composite primary key, then pass the key values in the order they are defined in model:
#Html.ActionLink("Edit", "Edit", new { comp = item.comp, part = item.part }) |
#Html.ActionLink("Details", "Details", new { comp = item.comp, part = item.part }) |
#Html.ActionLink("Delete", "Delete", new { comp = item.comp, part = item.part })
In the controller, receive both values:
public ActionResult Edit(int? comp, int? part)
{
DataView record = db.RecordDataView.Find(comp, part);
if (record == null)
{
return HttpNotFound();
}
return View(record);
}
On applying the above suggestion, I got the error :
The parameters dictionary contains a null entry for parameter 'isdel' of non-nullable type 'System.Boolean' for method 'System.Web.Mvc.ActionResult Edit(System.Nullable`1[System.Int32], Boolean)' in Controller'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
But as per this suggestion
I tried changing Details action signature to
public ActionResult Details(int eid,bool isdeleted) //same parameter name as in my Entity Model.
CRUD functionality with composite keys executes normally now.
Thanks #Jelle Oosterbosch

open a MVC4 view inside a jquery Dialog

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.

Resources