I'm using Orchard CMS v1.8.x. We've added the FlexSlider module and to make it link to specific areas of our site, we've added a field called "Slide Link" to the FlexSliderPart object.
Now, that all works pretty neat. But I have absolutely no idea how to reference this new field on the front-end. The FlexSliderWidgetViewModel only contains fields for Title and ImagePath. I have no idea how to retrieve the SlideLink (and SubTitle) field:
namespace Tekno.FlexSlider.ViewModels
{
public class FlexSliderWidgetViewModel
{
public string Title { get; set; }
public string ImagePath { get; set; }
}
}
And my View:
#using Tekno.FlexSlider.ViewModels
#{
Style.Require("FlexSlider");
Script.Require("FlexSlider");
var items = (IEnumerable<FlexSliderWidgetViewModel>)Model.SlideItems;
}
<div class="flexslider">
<ul class="slides">
#foreach (var item in items)
{
<li>
#if (item.ImagePath != "")
{
<img src="#Display.ResizeMediaUrl(Path: item.ImagePath, Height: 400, Mode: "crop")" />
}
<span class="slide-text">#item.Title</span>
</li>
}
</ul>
<span class="slide-text-bottom"></span>
</div>
The Driver's Display function:
protected override DriverResult Display(FlexSliderWidgetPart part, string displayType, dynamic shapeHelper)
{
var items = _contentManager.Query<FlexSliderPart, FlexSliderPartRecord>("FlexSlider")
.Where(i => i.GroupId == part.GroupId).OrderBy(i => i.Sort)
.List()
.Select(i => new FlexSliderWidgetViewModel()
{
ImagePath = ((MediaLibraryPickerField)i.Fields.Single(f => f.Name == "Picture"))
.MediaParts
.FirstOrDefault() == null ? "" : ((MediaLibraryPickerField)i.Fields.Single(f => f.Name == "Picture")).MediaParts.First().MediaUrl,
Title = i.Get<TitlePart>().Title
});
return ContentShape("Parts_FlexSliderWidget",
() => shapeHelper.Parts_FlexSliderWidget(SlideItems: items));
}
I had a quick look at the code and you won't be able to access those fields in the way the module is currently working. Basically it is accessing the content item in the drivers display method and then creating a View Model with the two bits of data (title and image)it deems necessary to send to the view. I would recommend changing the drivers Display method to send back the entire content item instead of that view model, you can then access fields you attach from the view directly.
If you don't have access to the driver you could I suppose grab the content manager in your view and redo all the work that driver is doing so you can access the content items. I wouldn't recommend this approach though...
This probably isn't the answer you were hoping for, sorry.
EDIT
This is basically pseudo code as I don't have access to the module to actually see if it works, but it should point you in, well, some sort of direction.
protected override DriverResult Display(FlexSliderWidgetPart part, string displayType, dynamic shapeHelper)
{
var items = _contentManager.Query<FlexSliderPart, FlexSliderPartRecord>("FlexSlider")
.Where(i => i.GroupId == part.GroupId).OrderBy(i => i.Sort)
.List();
return ContentShape("Parts_FlexSliderWidget",
() => shapeHelper.Parts_FlexSliderWidget(SlideItems: items));
}
then in your view:
#using Tekno.FlexSlider.ViewModels
#{
Style.Require("FlexSlider");
Script.Require("FlexSlider");
}
<div class="flexslider">
<ul class="slides">
#foreach (var part in Model.SlideItems)
{
dynamic item = part.ContentItem;
<li>
<img src="#Display.ResizeMediaUrl(Path: item.FlexSliderPart.MediaParts[0].MediaUrl, Height: 400, Mode: "crop")" />
<span class="slide-text">#item.TitlePart.Title</span>
</li>
}
</ul>
<span class="slide-text-bottom"></span>
</div>
Important thing here is the casting the content item to a dynamic so you can access all the fields etc. I've also never used the new Media stuff in 1.8 so... don't know if I'm accessing that correctly. I don't like it ^^
Related
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've been trying to write a simple accordian widget, where each section of accordian would be its own content part. I have the leafs content part created fine, but I want to create the accordian part which contains a list of the leafs. I havn't been able to find a good tutorial that went over something like this. I'm working on displaying the leafs now, and am running into issues. I'm trying to mimic the comments module. This is what I have. It seems like i am able to get up to the list of leafs in the driver, but i'm not sure what to do with the view. i see comments calls #Display(Model.List) but i have no idea what this is doing.
Edit View (Just using a textbox for one leaf id, need to figure out how to select leaves):
<fieldset>
<legend>Accordian Fields</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
#Html.TextBoxFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.AccordianLeaf)
</div>
<div class="editor-field">
#*#Html.DropDownListFor(m => m.AccordianLeaf,
new System.Web.Mvc.SelectList(service.GetComments(), "Value", "Text"))*#
#Html.TextBoxFor(model => model.AccordianLeaf)
#Html.ValidationMessageFor(model => model.AccordianLeaf)
</div>
</fieldset>
Model:
namespace SuccessCenter.Models
{
public class AccordianRecord : ContentPartRecord
{
public virtual string Title { get; set; }
public virtual int AccordianLeaf { get; set; }
}
public class AccordianPart : ContentPart<AccordianRecord>
{
[Required]
public string Title
{
get { return Retrieve(r => r.Title); }
set { Store(r => r.Title, value); }
}
[Required]
public int AccordianLeaf
{
get { return Retrieve(r => r.AccordianLeaf); }
set { Store(r => r.AccordianLeaf, value); }
}
}
}
Handler:
namespace SuccessCenter.Handlers
{
public class AccordianHandler : ContentHandler
{
public AccordianHandler(IRepository<AccordianRecord> repository)
{
Filters.Add(StorageFilter.For(repository));
}
}
}
Driver:
namespace SuccessCenter.Drivers
{
[UsedImplicitly]
public class AccordianDriver : ContentPartDriver<AccordianPart>
{
private readonly IAccordian _accordian;
public AccordianDriver(IAccordian accordian)
{
_accordian = accordian;
}
protected override DriverResult Display(AccordianPart part, string displayType, dynamic shapeHelper)
{
//return ContentShape("Parts_Accordian", () => shapeHelper.Parts_Accordian(Title: part.Title, AccordianLeaf: part.AccordianLeaf));
return Combined(
ContentShape("Parts_Accordian",
() =>
{
// create a hierarchy of shapes
var firstLevelShapes = new List<dynamic>();
var allShapes = new Dictionary<int, dynamic>();
var AccordianLeafs = _accordian.AccordianLeafs.ToList();
foreach (var item in AccordianLeafs)
{
var shape = shapeHelper.AccordianLeaf(ContentPart: item, ContentItem: item.ContentItem);
allShapes.Add(item.Id, shape);
}
var list = shapeHelper.List(Items: allShapes);
return shapeHelper.Parts_Accordian(
List: list
);
}));
}
//GET
protected override DriverResult Editor(AccordianPart part, dynamic shapeHelper)
{
return ContentShape("Parts_Accordian_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts/Accordian", Model: part, Prefix: Prefix));
}
//POST
protected override DriverResult Editor(AccordianPart part, IUpdateModel updater, dynamic shapeHelper)
{
updater.TryUpdateModel(part, Prefix, null, null);
return Editor(part, shapeHelper);
}
}
}
View:
#using SuccessCenter.Models;
}<div class="expand-view expanded">
<header class="bg-brand-blue txt-white relative">
<h3 class="txt-left">#Model.List.Title</h3>
<span class="toggle v-align absolute">
<span class="expanded">Colllapse <i class="icons icon-carat-up-wh"></i></span><span class="collapsed">Expand <i class="icons icon-carat-down-wh"></i></span>
</span>
</header>
<section class="default-padding">
#Model.List.AccordianLeaf
</section>
##Display(Model.List)#
The List shape (shapeHelper.List()) takes a range of content item shapes, which you seem to build with shapeHelper.AccordionLeaf(). Therefore you can just display it with the Display method:
#Display(Model.List)
This method will display the List property on your model (your model properties are the ones you give as parameter in shapeHelper.Parts_Accordian(/* model properties */)
I am not sure what you are trying to do in the view, it seems like your want to iterate over the items in the List shape? In that case you can do something like this:
#foreach (var item in Model.List.Items) {
// item here is an AccordionLeaf shape
#Display(item)
}
I've created a couple of custom regions within my Piranha CMS Installation but am having problems when I have any kind of text region within my custom region. The Editor will display and you can enter text but it doesn't save to the DB.
Here's my classes
using System;
using System.ComponentModel.Composition;
using Piranha.Extend;
using Piranha.Extend.Regions;
namespace MatchtechGroup.Models.Regions
{
[Export(typeof(IExtension))]
[ExportMetadata("InternalId", "SimpleTab")]
[ExportMetadata("Name", "Simple Tab")]
[ExportMetadata("Type", ExtensionType.Region)]
[Serializable]
public class SimpleTab : Extension, ITab
{
public string Title { get; set; }
public HtmlRegion Tab { get; set; }
public SimpleTab()
{
Tab = new HtmlRegion();
}
}
}
And my Manager template in Areas/Manager/Views/Extensions
#model MatchtechGroup.Models.Regions.SimpleTab
#{
Layout = "";
}
<ul class="form">
<li>
#Html.LabelFor(m => m.Title)
<div class="input">#Html.TextBoxFor(m => m.Title)</div>
</li>
<li>
#Html.TextAreaFor(m => m.Tab, new { #class = "editor", #rows = 10 })
</li>
</ul>
The manager interface renders my new region correctly in the page editor but will not save content from the Html Region. There are no errors displayed in the interface, I just don't get the 'This Page has saved' message bar appear or am I able to publish the page.
Any help would be much appreciated, feels like I'm missing something basic or just that I can't nest an HTML region within this custom region.
Thanks
The problem is probably that your HtmlValue gets invalidated in the Model Binder so that Model.IsValid is false. The easiest solution to your problem is to change your property to:
public class SimpleTab : ...
{
public string Title { get; set; }
public string Tab { get; set; }
}
The only difference would be when using it in the Razor markup. If you had a region of the SimpleTab type called MyTab the syntax would then be (for example):
<div>
<h3>#Model.Regions.MyTab.Title</h3>
<div class="content">
#Html.Raw(Model.Regions.MyTab.Tab)
</div>
</div>
The only difference is #Html.Raw() to make sure that the body isn't escaped.
Regards
/ HÃ¥kan
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.
I have MVC2 view in which there is a grid and image button. When image button is clicked, it will pass the data of grid's currentPage, filter and orderBy fields ... to another controller. The code is,
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MaintenanceEditableViewModel>" %>
"Image button"
<a href="<%: Url.Action("ExportToExcel", "ExportExcelButton", new { resourceId = ViewData["resourceId"], pagenum = ViewData["page"], orderBy = ViewData["orderBy"], filter = ViewData["filter"] }) %>">
<img src='<%: Url.Content("~/Content/Images/ExportExcelButton.gif") %>' /></a>
"Grid"
<%= Html.Telerik().Grid<MaintenanceAthletesResultsViewModel>()
.Name("Grid")
.TableHtmlAttributes(new { style = "font-size:8pt;" })
.HtmlAttributes(new { style = "height:335px" })
.DataKeys(k => k.Add(k2 => k2.ResultID))
.DataBinding(dataBinding => dataBinding.Ajax()
.Select("AthletesResultsAjax", "Maintenance")
...
"Action to initialize the grid in Maintenance controller"
public ActionResult AthletesResults(string id, bool editable)
{
ViewData["resourceId"] = id;
this._resourceBL.PopulateMaintenanceResourcesViewData(id, this.SelectedSport());
MaintenanceEditableViewModel model = new MaintenanceEditableViewModel { Editable = editable };
return View(model);
}
[GridAction]
public ActionResult AthletesResultsAjax(string id, int page, string orderBy, string filter)
{
List<MaintenanceAthletesResultsViewModel> model = null;
if (!string.IsNullOrEmpty(id))
{
ViewData["page"] = page;
ViewData["orderBy"] = orderBy;
ViewData["filter"] = filter;
model = this._resourceBL.GetMaintenanceAthletesResultsViewModel(int.Parse(id));
}
return View(new GridModel(model));
}
public ActionResult ExportToExcel(string resourceId, string pagenum, string orderBy, string filter)
The strange thing is that only ViewData["resourceId"] (set in initialization of view) is able to be passed. The 3 parameters, ViewData["page"], ViewData["orderBy"], ViewData["filter"] (set in grid ajax action) can not be passed. All 3 are null in ExportToExcel and so I think they are not correctly stored in ViewData (I am not sure how to check ViewData when image button is pressed). I am confused and need help on this.
Thanks
I don't see how/where you're sending those arguments into the AthletesResultsAjax action. The signature of the dataBinding.Ajax().Select() function you're using only specifies the action and the controller name. How is it going to know what those values are? You need to specify a routeValues object to send the appropriate data to the controller action. If you debug the code and put a breakpoint in your ajax action, do the actual values come through? I could be quite off here but I think your problem is more on this end than on how the data is being stored in the ViewData.
I think the problem is that ViewData is mainly used pass data from Controller to View and in your case you have Controller -> View -> Controller. I think Telerik Knowledge Base has a good solution how to provide additional data to your export action. Telerik Knowledge Base
<%= Html.ActionLink("Export to Excel", "Export", new { page = 1, orderBy = "~", filter = "~"}, new { id = "exportLink" }) %>
<script type="text/javascript">
function onDataBound() {
var grid = $(this).data('tGrid');
var $exportLink = $('#exportLink');
var href = $exportLink.attr('href');
href = href.replace(/page=([^&]*)/, 'page=' + grid.currentPage);
href = href.replace(/orderBy=([^&]*)/, 'orderBy=' + (grid.orderBy || '~'));
href = href.replace(/filter=(.*)/, 'filter=' + (grid.filterBy || '~'));
$exportLink.attr('href', href);
}
</script>
The script dynamically updates Export-links url when data is received & rendered (OnDataBound) to the screen. It should then bind nicely on the Controller side.
If you're more familiar with JavaScript then you should probably put an click event to the export button and make GET request in the JavaScript side. Then you don't have make that ugly looking url modifications.
I hope this helps!