28 Jan 2015
OK… this one was easy.
For starters, the objective was to find an easy to code and maintain a replacement for a WPF MVVM system (running as a WPF XAML Browser Application) that, among other functionality, allows for an user to open a “window” in the browser, input/choose some filters and preview a report that he or she could export to PDF or print.
Those filters are the usual kind: drop down lists, check boxes and radio buttons, date pickers, numeric spinners and normal text input boxes. Some come pre-filled (like the lists’ items that come from the database) and many are checked against the usual constraints: required, maximum length and pattern matching for example.
I first created a view model class that represented the filters. I also used this class to decorate any constrained members with the necessary data annotations. This step was mostly copy-and-paste. I had to replace some data types (like the ObservableCollection
that was replaced by ICollection<SelectListItem>
) but nothing major.
Example:
public class StatementReportViewModel {
public ICollection<SelectListItem> Funds { get; set; }
[Required]
public decimal UnitPrice { get; set; }
public DateTime? StartDate { get; set; }
public DateTime? EndDate { get; set; }
}
After that I created a view and coded a simple form:
@model StatementReportViewModel
@{
ViewBag.Title = "Statement report"
}
<h2>Statement report</h2>
@using (Html.BeginForm("Show", "StatementReport", FormMethod.Post, new { @class = "form-horizontal", role = "form", target = "_blank" }))
{
@Html.AntiForgeryToken()
<p>
<input type="submit" value="Preview" class="btn btn-default" />
</p>
@Html.ValidationSummary("", new { @class = "text-danger" })
<div class="form-group">
@Html.LabelFor(m => m.Funds, new { @class = "control-label col-md-2"})
<div class="col-md-10">
@Html.DropDownListFor(m => m.Funds, Model.Funds, new { @class = "form-control" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.StartDate, new { @class = "col-md-2 control-label" })
<div class="col-md-2">
@Html.EditorFor(m => m.StartDate, new { htmlAttributes = new { @class = "form-control", type = "date" } })
@Html.ValidationMessageFor(m => m.StartDate, "", new { @class = "text-danger" })
</div>
</div>
<div class="form-group">
@Html.LabelFor(m => m.EndDate, new { @class = "col-md-2 control-label" })
<div class="col-md-2">
@Html.EditorFor(m => m.EndDate, new { htmlAttributes = new { @class = "form-control", type = "date" } })
@Html.ValidationMessageFor(m => m.EndDate, "", new { @class = "text-danger" })
</div>
</div>}
<div class="form-group">
@Html.LabelFor(m => m.UnitPrice, new { @class = "col-md-2 control-label" })
<div class="col-md-2">
@Html.EditorFor(m => m.UnitPrice, new { htmlAttributes = new { @class = "form-control", type = "number", step = "0.01" } })
@Html.ValidationMessageFor(m => m.UnitPrice, "", new { @class = "text-danger" })
</div>
</div>
}
<div>
@Html.ActionLink("Back", "Index")
</div>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
To be able to render the report, I had to add the CrystalDecisions.CrystalReports.Engine
, CrystalDecisions.ReportSource
and CrystalDecisions.Shared
assemblies to the project’s references.
Then I copied both the dataset and the report files from the existing WPF project and simply pasted them in the new project (pasted the dataset inside the Models
folder and the report inside the Reports
folder that I had previously created).
Note that the report has to have a build action of Content
(it was embedded in the WPF project). I also use Copy if newer
so that it is properly updated when I build the project.
I also manually changed the namespaces to match the new project’s namespace.
Later, I added a new MVC controller (an empty one) and added two methods: an Index
one (to show a view with the filters) and a Show
method (so the page can POST
the filters and get the report back).
public class StatementReportController : Controller {
public ActionResult Index() {
var model = new StatementReportViewModel();
// Fill the model and return a View with it (not showing it here and not
// using async for now).
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Show(
int? funds,
decimal unitPrice,
DateTime? startDate,
DateTime? endDate
) {
var dataset = new StatementReportDataSet();
// Fill the dataset with the data (restricted according to the filters
// received). Again, not showed here and not using async for now either.
var report = new ReportDocument();
report.Load(Server.MapPath("~/Reports/StatementReport.rpt"));
report.SetDataSource(dataset);
report.SetParameterValue("pUnitPrice", unitPrice);
var stream = report.ExportToStream(
ExportFormatType.PortableDocFormat
);
return File(stream, "application/pdf");
}
}
Notes:
ReportDocument
is defined in the CrystalDecisions.CrystalReports.Engine
namespace.ExportFormatType
is defined in the CrystalDecisions.Shared
namespace.That was pretty much it.
It works as expected and I can use already existing code and assets almost as is.
Next, globalization!
This solution was heavily influenced by the following posts: