I would like to render an RDLC report in HTML within an ASP.NET MVC project.
I successfully made a prototype that renders an RDLC report in PDF, Excel, and TIFF image, with the help of this article. But I was surprised that HTML is not one of the default available formats in LocalReport.Render().
I came across this article, which describes a trick to enable the rendering format of HTML4.0, but I think that is only for a ReportViewer control (I could be wrong though).
The question is, in MVC how to render an RDLC report in HTML just like a ReportView does (see the screenshot below)?
This is a simple task. You can follow the following steps.
Create a folder in your solution and give a name Reports.
Add a ASP.Net web form and named it ReportView.aspx
Create a Class ReportData and add it to the Reports folder. Add the following code
to the Class.
public class ReportData
{
public ReportData()
{
this.ReportParameters = new List<Parameter>();
this.DataParameters = new List<Parameter>();
}
public bool IsLocal { get; set; }
public string ReportName { get; set; }
public List<Parameter> ReportParameters { get; set; }
public List<Parameter> DataParameters { get; set; }
}
public class Parameter
{
public string ParameterName { get; set; }
public string Value { get; set; }
}
Add another Class and named it ReportBasePage.cs. Add the following code in this Class.
public class ReportBasePage : System.Web.UI.Page
{
protected ReportData ReportDataObj { get; set; }
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
if (HttpContext.Current != null)
if (HttpContext.Current.Session["ReportData"] != null)
{
ReportDataObj = HttpContext.Current.Session["ReportData"] as ReportData;
return;
}
ReportDataObj = new ReportData();
CaptureRouteData(Page.Request);
}
private void CaptureRouteData(HttpRequest request)
{
var mode = (request.QueryString["rptmode"] + "").Trim();
ReportDataObj.IsLocal = mode == "local" ? true : false;
ReportDataObj.ReportName = request.QueryString["reportname"] + "";
string dquerystr = request.QueryString["parameters"] + "";
if (!String.IsNullOrEmpty(dquerystr.Trim()))
{
var param1 = dquerystr.Split(',');
foreach (string pm in param1)
{
var rp = new Parameter();
var kd = pm.Split('=');
if (kd[0].Substring(0, 2) == "rp")
{
rp.ParameterName = kd[0].Replace("rp", "");
if (kd.Length > 1) rp.Value = kd[1];
ReportDataObj.ReportParameters.Add(rp);
}
else if (kd[0].Substring(0, 2) == "dp")
{
rp.ParameterName = kd[0].Replace("dp", "");
if (kd.Length > 1) rp.Value = kd[1];
ReportDataObj.DataParameters.Add(rp);
}
}
}
}
}
Add ScriptManager to the ReportView.aspx page. Now Take a Report Viewer to the page. In report viewer set the property AsyncRendering="false". The code is given below.
<rsweb:ReportViewer ID="ReportViewerRSFReports" runat="server" AsyncRendering="false"
Width="1271px" Height="1000px" >
</rsweb:ReportViewer>
Add two NameSpace in ReportView.aspx.cs
using Microsoft.Reporting.WebForms;
using System.IO;
Change the System.Web.UI.Page to ReportBasePage. Just replace your code using the following.
public partial class ReportView : ReportBasePage
{
protected void Page_Load(object sender, EventArgs e)
{
if (!IsPostBack)
{
RenderReportModels(this.ReportDataObj);
}
}
private void RenderReportModels(ReportData reportData)
{
RASolarERPData dal = new RASolarERPData();
List<ClosingInventoryValuation> objClosingInventory = new List<ClosingInventoryValuation>();
// Reset report properties.
ReportViewerRSFReports.Height = Unit.Parse("100%");
ReportViewerRSFReports.Width = Unit.Parse("100%");
ReportViewerRSFReports.CssClass = "table";
// Clear out any previous datasources.
this.ReportViewerRSFReports.LocalReport.DataSources.Clear();
// Set report mode for local processing.
ReportViewerRSFReports.ProcessingMode = ProcessingMode.Local;
// Validate report source.
var rptPath = Server.MapPath(#"./Report/" + reportData.ReportName +".rdlc");
//#"E:\RSFERP_SourceCode\RASolarERP\RASolarERP\Reports\Report\" + reportData.ReportName + ".rdlc";
//Server.MapPath(#"./Report/ClosingInventory.rdlc");
if (!File.Exists(rptPath))
return;
// Set report path.
this.ReportViewerRSFReports.LocalReport.ReportPath = rptPath;
// Set report parameters.
var rpPms = ReportViewerRSFReports.LocalReport.GetParameters();
foreach (var rpm in rpPms)
{
var p = reportData.ReportParameters.SingleOrDefault(o => o.ParameterName.ToLower() == rpm.Name.ToLower());
if (p != null)
{
ReportParameter rp = new ReportParameter(rpm.Name, p.Value);
ReportViewerRSFReports.LocalReport.SetParameters(rp);
}
}
//Set data paramater for report SP execution
objClosingInventory = dal.ClosingInventoryReport(this.ReportDataObj.DataParameters[0].Value);
// Load the dataSource.
var dsmems = ReportViewerRSFReports.LocalReport.GetDataSourceNames();
ReportViewerRSFReports.LocalReport.DataSources.Add(new ReportDataSource(dsmems[0], objClosingInventory));
// Refresh the ReportViewer.
ReportViewerRSFReports.LocalReport.Refresh();
}
}
Add a Folder to the Reports Folder and named it Report. Now add a RDLC report to the Reports/Report folder and named it ClosingInventory.rdlc.
Now add a Controller and Named it ReportController. In to the controller add the following action method.
public ActionResult ReportViewer()
{
ViewData["reportUrl"] = "../Reports/View/local/ClosingInventory/";
return View();
}
Add a view page click on the ReportViewer Controller. Named the view page ReportViewer.cshtml. Add the following code to the view page.
#using (Html.BeginForm("Login"))
{
#Html.DropDownList("ddlYearMonthFormat", new SelectList(ViewBag.YearMonthFormat, "YearMonthValue", "YearMonthName"), new { #class = "DropDown" })
Stock In Transit: #Html.TextBox("txtStockInTransit", "", new { #class = "LogInTextBox" })
<input type="submit" onclick="return ReportValidationCheck();" name="ShowReport"
value="Show Report" />
}
Add an Iframe. Set the property of the Iframe as follows
frameborder="0" width="1000"; height="1000"; style="overflow:hidden;" scrolling="no"
Add Following JavaScript to the viewer.
function ReportValidationCheck() {
var url = $('#hdUrl').val();
var yearmonth = $('#ddlYearMonthFormat').val();
var stockInTransit = $('#txtStockInTransit').val()
if (stockInTransit == "") {
stockInTransit = 0;
}
if (yearmonth == "0") {
alert("Please Select Month Correctly.");
}
else {
//url = url + "dpSpYearMonth=" + yearmonth + ",rpYearMonth=" + yearmonth + ",rpStockInTransit=" + stockInTransit;
url = "../Reports/ReportView.aspx?rptmode=local&reportname=ClosingInventory¶meters=dpSpYearMonth=" + yearmonth + ",rpYearMonth=" + yearmonth + ",rpStockInTransit=" + stockInTransit;
var myframe = document.getElementById("ifrmReportViewer");
if (myframe !== null) {
if (myframe.src) {
myframe.src = url;
}
else if (myframe.contentWindow !== null && myframe.contentWindow.location !== null) {
myframe.contentWindow.location = url;
}
else { myframe.setAttribute('src', url); }
}
}
return false;
}
In Web.config file add the following key to the appSettings section add
key="UnobtrusiveJavaScriptEnabled" value="true"
In system.web handlers Section add the following key
add verb="*" path="Reserved.ReportViewerWebControl.axd" type = "Microsoft.Reporting.WebForms.HttpHandler, Microsoft.ReportViewer.WebForms, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
Change your data source as your own. This solution is very simple and I think every one enjoy it.
You can use the ReportViewer object to render an RDLC to PDF or HTML. For my case (below) I wanted a PDF document and I returned it as a FileContentResult ActionResult. If you want it to return as a download use the File ActionResult (I've commented that out for your use).
public ActionResult GetPackingSlipPDF(int shipmentId)
{
var shipment = _inboundShipmentService.GetInboundShipmentById(shipmentId);
Warning[] warnings;
string mimeType;
string[] streamids;
string encoding;
string filenameExtension;
var viewer = new ReportViewer();
viewer.LocalReport.ReportPath = #"Labels\PackingSlip.rdlc";
var shipLabel = new ShippingLabel { ShipmentId = shipment.FBAShipmentId, Barcode = GetBarcode(shipment.FBAShipmentId) };
viewer.LocalReport.DataSources.Add(new ReportDataSource("ShippingLabel", new List<ShippingLabel> { shipLabel }));
viewer.LocalReport.Refresh();
var bytes = viewer.LocalReport.Render("PDF", null, out mimeType, out encoding, out filenameExtension, out streamids, out warnings);
return new FileContentResult(bytes, mimeType);
//return File(bytes, mimeType, shipment.FBAShipmentId + "_PackingSlip.pdf");
}
Related
I am new to Razor page but have been working in aspx. This below is my code - please help me convert this to a Razor page:
void Page_Load(object sender, EventArgs e)
{
foreach(string f in Request.Files.AllKeys)
{
HttpPostedFile file = Request.Files[f];
file.SaveAs("C:\\e_data\\WorkPage\\IMS18\\ALBAB_Dynamic\\20008\\Case_Manager\\" + file.FileName);
}
}
I want to change to razor page code.
Here's what I use for uploading a single file and storing the path to the file in a database. It'll explain the bits that Microsoft left out of it's docs (for instance the path to the base directory in .netcore2.2) Note that security is not much of a concern for me as this is a small company intranet... but there's bits in there about getting filename without extension, and you may want to store without the file extension for security reasons (or remove and then add your own extension):
public async Task<IActionResult> OnPostAsync()
{
if (id == null)
{
return NotFound();
}
Kit = await _context.Kits.FirstOrDefaultAsync(m => m.ID == id);
if (Kit == null)
{
return NotFound();
}
if (Request.Form.Files.Count > 0)
{
IFormFile file = Request.Form.Files[0];
string folderName = "UploadedOriginalBOMs";
string OrgBOMRootPath = Path.Combine(AppContext.BaseDirectory, folderName);
if (!Directory.Exists(OrgBOMRootPath))
{
Directory.CreateDirectory(OrgBOMRootPath);
}
string sFileExtension = Path.GetExtension(file.FileName).ToLower();
string fullPath = Path.Combine(OrgBOMRootPath, file.FileName);
// StringBuilder sb = new StringBuilder();
if (file.Length > 0)
{
String cleanFilename = Path.GetFileNameWithoutExtension(file.FileName);
using (var stream = new FileStream(fullPath, FileMode.Create))
{
file.CopyTo(stream);
}
Kit.PathToOriginalBOM = "UploadedOriginalBOMs/" + file.FileName;
_context.Kits.Attach(Kit).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
}
else
{
if (!ModelState.IsValid)
{
return Page();
}
}
return RedirectToPage("./Index");
}
You'll notice that you can just use the same forloop as in your .aspx file.
previously Asp.Net MVC had this third party library which easily allowed uploading and reading from an excel file called Excel Data Reader. We didn't need to have the file on the local disk, which was great because my application needs to run on Azure.
However we are now porting this functionality to asp.net core 2, and it seems from searching that this is not possible. Does anybody know any libraries that would allow me to do this? Please note, I am not looking for solutions that read from a disk. I want to upload an excel file and read data from the stream directly.
I Could Read Excel File In 'Asp .Net Core' By This Code.
Import And Export Data Using EPPlus.Core.
[HttpPost]
public IActionResult ReadExcelFileAsync(IFormFile file)
{
if (file == null || file.Length == 0)
return Content("File Not Selected");
string fileExtension = Path.GetExtension(file.FileName);
if (fileExtension != ".xls" && fileExtension != ".xlsx")
return Content("File Not Selected");
var rootFolder = #"D:\Files";
var fileName = file.FileName;
var filePath = Path.Combine(rootFolder, fileName);
var fileLocation = new FileInfo(filePath);
using (var fileStream = new FileStream(filePath, FileMode.Create))
{
await file.CopyToAsync(fileStream);
}
if (file.Length <= 0)
return BadRequest(GlobalValidationMessage.FileNotFound);
using (ExcelPackage package = new ExcelPackage(fileLocation))
{
ExcelWorksheet workSheet = package.Workbook.Worksheets["Table1"];
//var workSheet = package.Workbook.Worksheets.First();
int totalRows = workSheet.Dimension.Rows;
var DataList = new List<Customers>();
for (int i = 2; i <= totalRows; i++)
{
DataList.Add(new Customers
{
CustomerName = workSheet.Cells[i, 1].Value.ToString(),
CustomerEmail = workSheet.Cells[i, 2].Value.ToString(),
CustomerCountry = workSheet.Cells[i, 3].Value.ToString()
});
}
_db.Customers.AddRange(customerList);
_db.SaveChanges();
}
return Ok();
}
I tried this code below (without using libs) for ASP.NET Core and it worked:
public ActionResult OnPostUpload(List<IFormFile> files)
{
try
{
var file = files.FirstOrDefault();
var inputstream = file.OpenReadStream();
XSSFWorkbook workbook = new XSSFWorkbook(stream);
var FIRST_ROW_NUMBER = {{firstRowWithValue}};
ISheet sheet = workbook.GetSheetAt(0);
// Example: var firstCellRow = (int)sheet.GetRow(0).GetCell(0).NumericCellValue;
for (int rowIdx = 2; rowIdx <= sheet.LastRowNum; rowIdx++)
{
IRow currentRow = sheet.GetRow(rowIdx);
if (currentRow == null || currentRow.Cells == null || currentRow.Cells.Count() < FIRST_ROW_NUMBER) break;
var df = new DataFormatter();
for (int cellNumber = {{firstCellWithValue}}; cellNumber < {{lastCellWithValue}}; cellNumber++)
{
//business logic & saving data to DB
}
}
}
catch(Exception ex)
{
throw new FileFormatException($"Error on file processing - {ex.Message}");
}
}
if we are talking about Razor Pages, here's a simple sample that I tested today..
Environ: .NET Core 3.1, VS 2019
A simple class
public class UserModel
{
public string Name { get; set; }
public string City { get; set; }
}
Index.cshtml.cs
usings..
using ExcelDataReader;
public void OnPost(IFormFile file)
{
List<UserModel> users = new List<UserModel>();
System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);
using (var stream = new MemoryStream())
{
file.CopyTo(stream);
stream.Position = 0;
using (var reader = ExcelReaderFactory.CreateReader(stream))
{
while (reader.Read()) //Each row of the file
{
users.Add(new UserModel { Name = reader.GetValue(0).ToString(), City = reader.GetValue(1).ToString()});
}
}
}
//users // you got the values here
}
Mark up in View
<form id="form1" method="post" enctype="multipart/form-data">
<div class="text-center">
<input type="file" id="file1" name="file" />
</div>
<script>
document.getElementById('file1').onchange = function () {
document.getElementById('form1').submit();
};
</script>
You would require ExcelDataReader nuget package, I used 3.6.0 version
github working code
Latest versions of ExcelDataReader support netstandard2.0, thus work with ASP.NET Core 2. It also targets netstandard1.3, so works with ASP.NET Core 1.x as well.
(not sure what you searched that said it is not possible, but that is clearly wrong)
First upload your excel file and read the excel file record using asp.net core 3.1.
using System;
using Microsoft.AspNetCore.Mvc;
using ExcelFileRead.Models;
using Microsoft.AspNetCore.Hosting;
using System.IO;
using OfficeOpenXml;
using System.Linq;
namespace ExcelFileRead.Controllers
{
public class HomeController : Controller
{
private readonly IHostingEnvironment _hostingEnvironment;
public HomeController(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment;
}
public ActionResult File()
{
FileUploadViewModel model = new FileUploadViewModel();
return View(model);
}
[HttpPost]
public ActionResult File(FileUploadViewModel model)
{
string rootFolder = _hostingEnvironment.WebRootPath;
string fileName = Guid.NewGuid().ToString() + model.XlsFile.FileName;
FileInfo file = new FileInfo(Path.Combine(rootFolder, fileName));
using (var stream = new MemoryStream())
{
model.XlsFile.CopyToAsync(stream);
using (var package = new ExcelPackage(stream))
{
package.SaveAs(file);
}
}
using (ExcelPackage package = new ExcelPackage(file))
{
ExcelWorksheet worksheet = package.Workbook.Worksheets.FirstOrDefault();
if (worksheet == null)
{
//return or alert message here
}
else
{
var rowCount = worksheet.Dimension.Rows;
for (int row = 2; row <= rowCount; row++)
{
model.StaffInfoViewModel.StaffList.Add(new StaffInfoViewModel
{
FirstName = (worksheet.Cells[row, 1].Value ?? string.Empty).ToString().Trim(),
LastName = (worksheet.Cells[row, 2].Value ?? string.Empty).ToString().Trim(),
Email = (worksheet.Cells[row, 3].Value ?? string.Empty).ToString().Trim(),
});
}
}
}
return View(model);
}
}
}
For more details(step by step)
https://findandsolve.com/articles/how-to-read-column-value-from-excel-in-aspnet-core-or-best-way-to-read-write-excel-file-in-dotnet-core
I have a Create action that takes an entity object and a HttpPostedFileBase image. The image does not belong to the entity model.
I can save the entity object in the database and the file in disk, but I am not sure how to validate these business rules:
Image is required
Content type must be "image/png"
Must not exceed 1MB
A custom validation attribute is one way to go:
public class ValidateFileAttribute : RequiredAttribute
{
public override bool IsValid(object value)
{
var file = value as HttpPostedFileBase;
if (file == null)
{
return false;
}
if (file.ContentLength > 1 * 1024 * 1024)
{
return false;
}
try
{
using (var img = Image.FromStream(file.InputStream))
{
return img.RawFormat.Equals(ImageFormat.Png);
}
}
catch { }
return false;
}
}
and then apply on your model:
public class MyViewModel
{
[ValidateFile(ErrorMessage = "Please select a PNG image smaller than 1MB")]
public HttpPostedFileBase File { get; set; }
}
The controller might look like this:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// The uploaded image corresponds to our business rules => process it
var fileName = Path.GetFileName(model.File.FileName);
var path = Path.Combine(Server.MapPath("~/App_Data"), fileName);
model.File.SaveAs(path);
return Content("Thanks for uploading", "text/plain");
}
}
and the view:
#model MyViewModel
#using (Html.BeginForm("Index", "Home", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
#Html.LabelFor(x => x.File)
<input type="file" name="#Html.NameFor(x => x.File)" id="#Html.IdFor(x => x.File)" />
#Html.ValidationMessageFor(x => x.File)
<input type="submit" value="upload" />
}
Based on Darin Dimitrov's answer which I have found very helpful, I have an adapted version which allows checks for multiple file types, which is what I was initially looking for.
public override bool IsValid(object value)
{
bool isValid = false;
var file = value as HttpPostedFileBase;
if (file == null || file.ContentLength > 1 * 1024 * 1024)
{
return isValid;
}
if (IsFileTypeValid(file))
{
isValid = true;
}
return isValid;
}
private bool IsFileTypeValid(HttpPostedFileBase file)
{
bool isValid = false;
try
{
using (var img = Image.FromStream(file.InputStream))
{
if (IsOneOfValidFormats(img.RawFormat))
{
isValid = true;
}
}
}
catch
{
//Image is invalid
}
return isValid;
}
private bool IsOneOfValidFormats(ImageFormat rawFormat)
{
List<ImageFormat> formats = getValidFormats();
foreach (ImageFormat format in formats)
{
if(rawFormat.Equals(format))
{
return true;
}
}
return false;
}
private List<ImageFormat> getValidFormats()
{
List<ImageFormat> formats = new List<ImageFormat>();
formats.Add(ImageFormat.Png);
formats.Add(ImageFormat.Jpeg);
formats.Add(ImageFormat.Gif);
//add types here
return formats;
}
}
Here is a way to do it using viewmodel, take a look at whole code here
Asp.Net MVC file validation for size and type
Create a viewmodel as shown below with FileSize and FileTypes
public class ValidateFiles
{
[FileSize(10240)]
[FileTypes("doc,docx,xlsx")]
public HttpPostedFileBase File { get; set; }
}
Create custom attributes
public class FileSizeAttribute : ValidationAttribute
{
private readonly int _maxSize;
public FileSizeAttribute(int maxSize)
{
_maxSize = maxSize;
}
//.....
//.....
}
public class FileTypesAttribute : ValidationAttribute
{
private readonly List<string> _types;
public FileTypesAttribute(string types)
{
_types = types.Split(',').ToList();
}
//....
//...
}
And file length validation in asp.net core:
public async Task<IActionResult> MyAction()
{
var form = await Request.ReadFormAsync();
if (form.Files != null && form.Files.Count == 1)
{
var file = form.Files[0];
if (file.Length > 1 * 1024 * 1024)
{
ModelState.AddModelError(String.Empty, "Maximum file size is 1 Mb.");
}
}
// action code goes here
}
You may want to consider saving the image to database also:
using (MemoryStream mstream = new MemoryStream())
{
if (context.Request.Browser.Browser == "IE")
context.Request.Files[0].InputStream.CopyTo(mstream);
else
context.Request.InputStream.CopyTo(mstream);
if (ValidateIcon(mstream))
{
Icon icon = new Icon() { ImageData = mstream.ToArray(), MimeType = context.Request.ContentType };
this.iconRepository.SaveOrUpdate(icon);
}
}
I use this with NHibernate - entity defined:
public Icon(int id, byte[] imageData, string mimeType)
{
this.Id = id;
this.ImageData = imageData;
this.MimeType = mimeType;
}
public virtual byte[] ImageData { get; set; }
public virtual string MimeType { get; set; }
Then you can return the image as a FileContentResult:
public FileContentResult GetIcon(int? iconId)
{
try
{
if (!iconId.HasValue) return null;
Icon icon = this.iconRepository.Get(iconId.Value);
return File(icon.ImageData, icon.MimeType);
}
catch (Exception ex)
{
Log.ErrorFormat("ImageController: GetIcon Critical Error: {0}", ex);
return null;
}
}
Note that this is using ajax submit. Easier to access the data stream otherwise.
I need an html helper that would take care of "tabs" functionality on a page. clicking on the tabs will re-load the page and reload the partial view (if specified). I wrote it like so but not sure it this is the best solution.??
public static class TabExtensions
{
public static MvcHtmlString Tabs(this HtmlHelper htmlHelper, List<TabItem> tabItems, object htmlAttributes = null)
{
if (tabItems == null)
{
throw new ArgumentException("at least one tab item required");
}
string viewName = string.Empty;
object model = null;
var sb = new StringBuilder();
sb.Append("<a name=\"tabs\"></a>");
var tagUl = new TagBuilder("ul");
tagUl.MergeAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
// Current url data
var baseUri = new UriBuilder(htmlHelper.ViewContext.HttpContext.Request.Url);
var selTab = htmlHelper.ViewContext.RequestContext.HttpContext.Request.QueryString["tab"];
foreach (var tab in tabItems)
{
// No tab user selected
if (string.IsNullOrEmpty(selTab))
{
selTab = tab.TabLinkText;
}
var tagLi = new TagBuilder("li");
string tagInnerHtml;
if (selTab.Equals(tab.TabLinkText, StringComparison.OrdinalIgnoreCase))
{
tagLi.MergeAttribute("class", "current");
tagInnerHtml = string.Format("<strong>{0}</strong>", tab.Text);
viewName = tab.PartialViewName;
model = tab.PartialViewModel;
}
else
{
tagInnerHtml = tab.Text;
}
var queryToAppend = string.Concat("tab=", tab.TabLinkText);
var querystring = new StringBuilder();
if (baseUri.Query.Length > 1)
{
if (baseUri.Query.Contains("tab"))
{
querystring.Append(baseUri.Query.Replace(string.Concat("tab=", selTab), queryToAppend));
}
else
{
querystring.Append(baseUri.Query + "&" + queryToAppend);
}
}
else
{
querystring.Append("?" + queryToAppend);
}
// Assign anchor link
querystring.Append("#tabs");
tagLi.InnerHtml = string.Format("{1}", querystring, tagInnerHtml);
tagUl.InnerHtml += tagLi.ToString();
}
sb.Append(tagUl.ToString());
// Render partial
if (!string.IsNullOrEmpty(viewName))
{
htmlHelper.ViewData.Model = model;
using (StringWriter sw = new StringWriter())
{
ViewEngineResult viewResult = ViewEngines.Engines.FindPartialView(htmlHelper.ViewContext.Controller.ControllerContext, viewName);
ViewContext viewContext = new ViewContext(htmlHelper.ViewContext.Controller.ControllerContext, viewResult.View, htmlHelper.ViewData, htmlHelper.ViewContext.TempData, sw);
viewResult.View.Render(viewContext, sw);
sb.Append(sw.GetStringBuilder().ToString());
}
}
return MvcHtmlString.Create(sb.ToString());
}
}
#region Tab Item Model
public class TabItem
{
public string Text { get; set; }
public string TabLinkText { get; set; }
public string PartialViewName { get; set; }
public object PartialViewModel { get; set; }
public TabItem(string text, string tabLinkText)
: this()
{
this.Text = text;
this.TabLinkText = tabLinkText;
}
public TabItem(string text, string tabLinkText, string partialViewName, object partialViewModel = null)
: this()
{
this.Text = text;
this.TabLinkText = tabLinkText;
this.PartialViewName = partialViewName;
this.PartialViewModel = partialViewModel;
}
public TabItem()
{
this.Text = string.Empty;
this.PartialViewName = string.Empty;
this.TabLinkText = string.Empty;
this.PartialViewModel = null;
}
}
#endregion
You use it like so:
<%
var tabList = new List<TabItem>
{
new TabItem(LocalResources.fld_AboutFirm_lbl, "about"),
new TabItem(LocalResources.fld_FirmOffers_lbl, "offer"),
new TabItem(LocalResources.fld_Profile_lbl, "profile", "~/Views/Partial/FirmProfileTab.cshtml", Model),
new TabItem(LocalResources.fld_Contact_lbl, "contact", "~/Views/Partial/FirmContactTab.cshtml", Model)
};
%>
<%: Html.Tabs(tabList, new { #class = "firmTabs clearfix" })%>
this will generate html:
<ul class="firmTabs clearfix"><li>O firmie</li><li>Firma oferuje</li><li>Profil</li><li class="current"><strong>Kontakt</strong></li></ul>
I think the better solution is to make master page for tab menu and view pages for tab content. Your approach looks very complex for me. Why do you need html helper for this? If you encapsulate your html into helper method - you loose your view. So in terms of MVC your way is not good I think.
I've got the following code in my master page to load some customization stuff, like a css file, some address info in the footer, a header/footer logo etc. And I'm faced with load times of up to a minute! This is terrible practice, and I know - I just hacked it out to make it work. What would be the best practice of loading this type of customization information?
Currently, I try to load a cookie - check if it has the required keys, and if it doesnt - load the information from the database. I would like to have something similar to a .config file that's cached on the clients machine, if possible.
All the information is stored in a table called StoreSettings, which is linked to the Reseller table with StoreSettingsID.
<head id="Head1">
<%
HttpCookie storeSettingsCookie = Request.Cookies["StoreSettings"];
try
{
if (storeSettingsCookie == null || storeSettingsCookie.HasKeys == false)
{
if (Context.User.Identity.IsAuthenticated && Context.User.IsInRole("Reseller"))
{
var reseller = new Reseller();
var storeSettings = new StoreSettings();
var resellerRepository = new ResellerRepository();
reseller = resellerRepository.GetResellerByUsername(Context.User.Identity.Name);
if (reseller.StoreSettingsID != null && reseller.StoreSetting.Theme != null)
{
var storeSettingsRepository = new StoreSettingsRepository();
storeSettings = storeSettingsRepository.GetStoreSettings((int)reseller.StoreSettingsID);
storeSettingsCookie = new HttpCookie("StoreSettings");
storeSettingsCookie["HeaderImage"] = storeSettings.Image1.FileName;
storeSettingsCookie["FooterImage"] = storeSettings.Image.FileName;
storeSettingsCookie["ThemeLocation"] = storeSettings.Theme.StylesheetLocation;
storeSettingsCookie["StoreName"] = storeSettings.StoreName;
storeSettingsCookie["Address1"] = storeSettings.Address1;
storeSettingsCookie["Address2"] = storeSettings.Address2;
storeSettingsCookie["City"] = storeSettings.City;
storeSettingsCookie["PostalCode"] = storeSettings.PostalCode;
storeSettingsCookie["ProvinceCode"] = storeSettings.Province.Abbreviation;
storeSettingsCookie["Phone"] = storeSettings.Phone;
Response.Cookies.Add(storeSettingsCookie);
}
else
{
storeSettingsCookie = new HttpCookie("StoreSettings");
storeSettingsCookie["ThemeLocation"] = "~/Content/jquery-ui-1.8.9.custom.css";
storeSettingsCookie["StoreName"] = "";
storeSettingsCookie["Address1"] = "";
Response.Cookies.Add(storeSettingsCookie);
}
}
}
}
catch
{
}
%>
<title>
<asp:ContentPlaceHolder ID="TitleContent" runat="server" />
|
<%: string.IsNullOrEmpty(storeSettingsCookie["StoreName"]) ? "My Store Name" : storeSettingsCookie["StoreName"] %>
</title>
... <%-- Css/JS --%>
</head>
Any suggestions are appreciated. I expect a lot of sighs from the MVC guys. I know this isn't how MVC is supposed to work, so please refrain from reminding me of that, haha. :)
edit
Okay, so following LukLed's advice, I've created a base controller class, stuck the code above in its constructor and had my controllers inherit it. This appears like it is going to work, however the User object is null. How should I work around this? Here's what I've got:
public BaseController()
{
var resellerRepository = new ResellerRepository();
var reseller = resellerRepository.GetResellerByUsername(User.Identity.Name);
if (reseller.StoreSettingsID != null && reseller.StoreSetting.Theme != null)
{
var storeSettingsRepository = new StoreSettingsRepository();
var storeSettings = storeSettingsRepository.GetStoreSettings((int)reseller.StoreSettingsID);
ViewData["HeaderImage"] = storeSettings.Image1.FileName;
ViewData["FooterImage"] = storeSettings.Image.FileName;
ViewData["ThemeLocation"] = storeSettings.Theme.StylesheetLocation;
ViewData["StoreName"] = storeSettings.StoreName;
ViewData["Address1"] = storeSettings.Address1;
ViewData["Address2"] = storeSettings.Address2;
ViewData["City"] = storeSettings.City;
ViewData["PostalCode"] = storeSettings.PostalCode;
ViewData["ProvinceCode"] = storeSettings.Province.Abbreviation;
ViewData["Phone"] = storeSettings.Phone;
}
else
{
ViewData["ThemeLocation"] = "~/Content/jquery-ui-1.8.9.custom.css";
ViewData["StoreName"] = "";
ViewData["Address1"] = "";
}
}
The reseller must be logged on in order to view the store.
edit
So here is where I am at after following Darins advice - I upgraded to MVC3 and used a GlobalActionFilter, which is executing after EVERY action called. How do I prevent this - because it executes 4-5 times. Also - the viewdata is null every time. What am I doing wrong?
Here is my action filter (I didn't use Automapper from Darin's example because StoreSettings doesn't translate directly to StoreSettingsViewModel, and I wanted to see this working, first)
public class StoreSettingsActionFilter : ActionFilterAttribute
{
private readonly IResellerRepository _resellerRepository;
private readonly IStoreSettingsRepository _storeSettingsRepository;
public StoreSettingsActionFilter(
IResellerRepository resellerRepository,
IStoreSettingsRepository storeSettingsRepository
)
{
_resellerRepository = resellerRepository;
_storeSettingsRepository = storeSettingsRepository;
}
public StoreSettingsActionFilter()
: this(new ResellerRepository(), new StoreSettingsRepository())
{
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
var settingsViewModel = new StoreSettingsViewModel();
settingsViewModel.ThemeLocation = "~/Content/jquery-ui-1.8.9.custom.css";
var user = filterContext.HttpContext.User;
if (!user.Identity.IsAuthenticated || !user.IsInRole("Reseller"))
{
filterContext.Controller.ViewData["storeSettings"] = settingsViewModel;
return;
}
var session = filterContext.HttpContext.Session;
var reseller = session["reseller"] as Reseller;
if (reseller == null)
{
reseller = _resellerRepository.GetResellerByUsername(user.Identity.Name);
session["reseller"] = reseller;
}
if (reseller.StoreSettingsID != null && reseller.StoreSetting.Theme != null)
{
var storeSettings = session["storeSettings"] as StoreSettings;
if (storeSettings == null)
{
storeSettings = _storeSettingsRepository.GetStoreSettings((int)reseller.StoreSettingsID);
session["storeSettings"] = storeSettings;
}
// Using AutoMapper to convert between the model and the view model
//settingsViewModel = Mapper.Map<StoreSettings, StoreSettingsViewModel>(storeSettings);
settingsViewModel.ThemeLocation = storeSettings.Theme.StylesheetLocation;
settingsViewModel.Address1 = storeSettings.Address1;
settingsViewModel.Address2 = storeSettings.Address2;
settingsViewModel.City = storeSettings.City;
settingsViewModel.FooterImage = storeSettings.Image.FileName;
settingsViewModel.HeaderImage = storeSettings.Image1.FileName;
settingsViewModel.Phone = storeSettings.Phone;
settingsViewModel.PostalCode = storeSettings.PostalCode;
settingsViewModel.ProvinceCode = storeSettings.Province.Abbreviation;
settingsViewModel.StoreName = storeSettings.StoreName;
}
filterContext.Controller.ViewData["storeSettings"] = settingsViewModel;
}
}
Here is where i register the global action filter
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
//Register the global action filter
GlobalFilters.Filters.Add(new StoreSettingsActionFilter());
//RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
I would use an action filter that will inject the store settings on each request. The view model could look like this:
public class StoreSettingsViewModel
{
public string HeaderImage { get; set; }
public string FooterImage { get; set; }
public string ThemeLocation { get; set; }
public string StoreName { get; set; }
public string Address1 { get; set; }
public string Address2 { get; set; }
public string City { get; set; }
public string PostalCode { get; set; }
public string ProvinceCode { get; set; }
public string Phone { get; set; }
}
And the action filter:
public class StoreSettingsActionFilter : ActionFilterAttribute
{
private readonly IResellerRepository _resellerRepository;
private readonly IStoreSettingsRepository _storeSettingsRepository;
public StoreSettingsActionFilter(
IResellerRepository resellerRepository,
IStoreSettingsRepository storeSettingsRepository
)
{
_resellerRepository = resellerRepository;
_storeSettingsRepository = storeSettingsRepository;
}
public StoreSettingsActionFilter()
: this(new ResellerRepository(), new StoreSettingsRepository())
{ }
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
var settingsViewModel = new StoreSettingsViewModel();
settingsViewModel.ThemeLocation = "~/Content/jquery-ui-1.8.9.custom.css";
var user = filterContext.HttpContext.User;
if (!user.Identity.IsAuthenticated || !user.IsInRole("Reseller"))
{
filterContext.Controller.ViewData["storeSettings"] = settingsViewModel;
return;
}
var session = filterContext.HttpContext;
var reseller = session["reseller"] as Reseller;
if (reseller == null)
{
reseller = _resellerRepository.GetResellerByUsername(user.Identity.Name);
session["reseller"] = reseller;
}
if (reseller.StoreSettingsID != null && reseller.StoreSetting.Theme != null)
{
var storeSettings = session["storeSettings"] as StoreSettings;
if (storeSettings == null)
{
storeSettings = _storeSettingsRepository.GetStoreSettings((int)reseller.StoreSettingsID);
session["storeSettings"] = storeSettings;
}
// Using AutoMapper to convert between the model and the view model
settingsViewModel = Mapper.Map<StoreSettings, StoreSettingsViewModel>(storeSettings);
}
filterContext.Controller.ViewData["storeSettings"] = settingsViewModel;
}
}
Now we need to apply this attribute on the base controller so that it is executed for each action:
[StoreSettings]
public abstract class BaseController : Controller
{
}
If you are using ASP.NET MVC 3 you could have a global action filter.
And finally inside the master page you would have access to the store settings:
<%
var storeSettings = (StoreSettingsViewModel)ViewData["storeSettings"];
%>
<title>
<asp:ContentPlaceHolder ID="TitleContent" runat="server" />
<%: storeSettings.StoreName ?? "My Store Name" %>
</title>
<%-- Css/JS --%>
...