JSF: Automatically replace Tags in Strings with CommandLinks - jsf-2.2

i have a String like "Hello [everyone]!". "Everyone" should be replaced with a commandLink that points to an Page-Object.
For Example:
My Code detects "[everyone]" and creates a new "Page" with the headline "everyone" in my JavaDB Database. Now i want that [everyone] will shown as an commandLink:
Hello <h:commandLink value="everyone" action="#{PageController.getPage(everyone)}" />
or something else.
ATM i have this code to display the text with the []-Tags:
<h:outputText value="#{PageController.currentPage.latestContent.text}" />
Now what is the best practise to replace Tags (i.e. [XYZ]) with a specific commandLink? Or rather: how i can replace substrings with JSF-Tags (they should be rendered)? I have only found the possibility to create converter, but only examples to convert the complete string. :/ To find out the right substring i use Regulary Expressions.
Merry Christmas :)

My Soluion:
#ApplicationScoped
#Named("TagViewPhase")
public class TagViewPhase implements Serializable {
Application app;
public void beforePhase(PhaseEvent event) {
if (app == null) {
app = event.getFacesContext().getApplication();
}
if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) {
scanForPageContent(event.getFacesContext().getViewRoot());
}
}
private void scanForPageContent(UIComponent component) {
for (UIComponent i : component.getChildren()) {
if ("pageContent".equals(i.getId())) {
HtmlOutputText content = (HtmlOutputText) i;
HtmlPanelGroup group = generatePageContent(content);
content.getParent().getChildren().add(group);
content.getParent().getChildren().remove(i);
} else {
scanForPageContent(i);
}
}
}
private HtmlPanelGroup generatePageContent(final HtmlOutputText pContent) {
List<UIComponent> childTree = new ArrayList<>();
String content = pContent.getValue().toString();
Pattern pattern = Pattern.compile("\\[(.*?)\\]");
Matcher matcher = pattern.matcher(content);
Integer start, end = 0;
String linkValue;
while (matcher.find()) {
start = matcher.start();
if (end < start) {
HtmlOutputText htmlText = (HtmlOutputText) app.createComponent(HtmlOutputText.COMPONENT_TYPE);
htmlText.setValue(content.substring(end, start));
childTree.add(htmlText);
}
end = matcher.end();
linkValue = content.substring(start + 1, end - 1);
HtmlCommandLink link = (HtmlCommandLink) app.createComponent(HtmlCommandLink.COMPONENT_TYPE);
link.setValue(linkValue);
link.setActionExpression(JSFExpression.createMethodExpression(
"#{PageController.switchPageByString('" + linkValue + "')}",
String.class,
new Class<?>[]{}
));
childTree.add(link);
}
if (end < content.length()) {
HtmlOutputText htmlText = (HtmlOutputText) app.createComponent(HtmlOutputText.COMPONENT_TYPE);
htmlText.setValue(content.substring(end, content.length()));
childTree.add(htmlText);
}
HtmlPanelGroup group = (HtmlPanelGroup) app.createComponent(HtmlPanelGroup.COMPONENT_TYPE);
group.getChildren().addAll(childTree);
return group;
}
}
xhtml File:
<f:view beforePhase="#{TagViewPhase.beforePhase}">
<h:panelGroup>
<h:outputText id="pageContent" value="#{PageController.currentPageContent.text}" />
</h:panelGroup>
</f:view>

Related

APEX - Why aren't my input validations working?

I'm working on a contact search page and I have a problem. My console has no errors but when I run a search, the results don't generate and all of my input validation errors prompt.
For Example:
Jane Grey
The Search for Jane Grey
Here is my VisualForce page:
<apex:page id="ContactPage" controller="Ctrl_ContactSearch">
<apex:tabPanel id="ContactPanel">
<apex:tab id="ContactTab" label="Contact Search">
<apex:form id="ContactForm">
<!-- Input Fields -->
<apex:pageBlock title="Contact Search Page" id="ContactBlock">
<apex:pageBlockSection id="contact-table" columns="3">
<apex:pageBlockSectionItem id="NameInputBlock">
<apex:outputLabel value="Name" />
<apex:inputText id="NameInputField" value="{!name}" />
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem id="PhoneInputBlock">
<apex:outputLabel value="Phone" />
<apex:inputText id="PhoneInputField" value="{!phone}" />
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem id="EmailInputBlock">
<apex:outputLabel value="Email" />
<apex:inputText id="EmailInputField" value="{!email}" />
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
<!-- Buttons -->
<apex:pageBlockButtons location="bottom">
<apex:commandButton value="Search Contacts" action="{!searchContacts}" />
<apex:commandButton value="Clear Fields" action="{!ClearFields}" />
</apex:pageBlockButtons>
</apex:pageBlock>
<!-- Results Display -->
<apex:pageBlock title="Results" rendered="{!contacts.size!=0}" >
<apex:pageBlockSection >
<apex:pageBlockTable value="{!contacts}" var="c" id="contact-table">
<apex:column >
<apex:facet name="header">Name</apex:facet>
{!c.Name}
</apex:column>
<apex:column >
<apex:facet name="header">Phone Number</apex:facet>
{!c.Phone}
</apex:column>
<apex:column >
<apex:facet name="header">Email</apex:facet>
{!c.Email}
</apex:column>
</apex:pageBlockTable>
</apex:pageBlockSection>
</apex:pageBlock>
<!-- Error Display -->
<apex:pageBlock title="Errors" id="ErrorSection" >
<apex:pageMessages id="ErrorsListing">
</apex:pageMessages>
</apex:pageBlock>
</apex:form>
</apex:tab>
</apex:tabPanel>
<script type = "text/javascript">
var name = document.getElementByID("name");
var phone = document.getElementByID("phone");
var email = document.getElementByID("email");
</script>
</apex:page>
Here is my Controller:
public with sharing class Ctrl_ContactSearch
{
public List<Contact> contacts { get; set; }
public String name { get; set; }
public String phone { get; set; }
public String email { get; set; }
public integer MatchingContacts { get; set; }
public boolean ErrorsPresent = false;
public boolean Error_NoResults = false;
public boolean Error_FieldsEmpty = false;
public boolean Error_NameSyntax = false;
public boolean Error_PhoneSyntax = false;
public boolean Error_EmailSyntax = false;
/* Name Input Validation Regex*/
public static Boolean ValidationName (String name)
{
Boolean NameIsValid = false;
String nameRegex = '/^[a-zA-Z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*(__[cC])?$/';
Pattern PatternName = Pattern.compile(nameRegex);
Matcher nameMatcher = PatternName.matcher(name);
if (nameMatcher.matches())
{
NameIsValid = true;
}
return NameIsValid;
}
/* Phone Input Validation Regex*/
public static Boolean ValidationPhone (String phone)
{
Boolean PhoneIsValid = false;
String phoneRegex = '//^\\(?([0-9]{3})\\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$//';
Pattern PatternPhone = Pattern.compile(phoneRegex);
Matcher phoneMatcher = PatternPhone.matcher(phone);
if (phoneMatcher.matches())
{
PhoneIsValid = true;
}
return PhoneIsValid;
}
/* Email Input Validation Regex*/
public static Boolean ValidationEmail (String email)
{
Boolean EmailIsValid = false;
String emailRegex = '/^\\w+([\\.-]?\\w+)*#\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$//';
Pattern PatternEmail = Pattern.compile(emailRegex);
Matcher emailMatcher = PatternEmail.matcher(email);
if (emailMatcher.matches())
{
EmailIsValid = true;
}
return EmailIsValid;
}
/* Runs when "Clear Fields" button is pressed*/
public void ClearFields()
{
name = '';
phone = '';
email = '';
}
/* Runs when "Search Contacts" button is pressed*/
public PageReference searchContacts()
{
/* Checks if input fields are empty*/
if (name == '' && phone == '' && email == '')
{
Error_FieldsEmpty = true;
ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR,'Your fields are blank. Please enter your information in the remaining fields to proceed'));
}
else
{
/* Checks Input Validation Results*/
if (ValidationName(name) == false)
{
Error_NameSyntax = true;
ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR,'Inproper name format. Please enter name in following format : Firstname Lastname.'));
}
if (ValidationPhone(phone) == false)
{
Error_PhoneSyntax = true;
ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR,'Inproper phone number format. Please enter number in following format : XXX-XXX-XXXX.'));
}
if (ValidationPhone(email) == false)
{
Error_EmailSyntax = true;
ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR,'Inproper email format. Please enter email in following format : XXX#emailprovider.com'));
}
/* Runs if all Validation results are 'true'*/
if (ValidationName(name) == true && ValidationPhone(phone) == true && ValidationPhone(email) == true)
{
contacts = [select Name, Phone, Email from Contact where Name = :name or Phone = :phone or email = :email];
MatchingContacts = contacts.size();
/* Checks if/how many matches were found from the search*/
if(MatchingContacts != 0)
{
system.debug('There are currently ' + MatchingContacts + 'results found.' );
}
else
{
Error_NoResults = false;
ApexPages.addmessage(new ApexPages.message(ApexPages.severity.ERROR,'No matching Contacts were found.'));
}
}
}
/* Displays "Error" field if any errors are found*/
if (Error_NoResults == true || Error_FieldsEmpty == true || Error_NameSyntax == true || Error_PhoneSyntax == true || Error_EmailSyntax == true)
{
ErrorsPresent = true;
ApexPages.addmessage(new ApexPages.message(ApexPages.severity.WARNING,'There are errors present. Please read and follow instructions accordingly.'));
}
return null;
}
}
What do I have wrong here?
For some reason StackOverflow won't let me make this post without adding extra text. I'm putting this here to increase the wordcount.
1st, some constructive criticism:
The DRY principle says you should combine your validation methods into 1.
2nd, it looks like your regexs weren't good. Depending on the exact requirements of the validation, you can try these below... Taken willy nilly from other replies to other questions on forums much like this one... (I am by no means a Regex expert)
Try something like this:
private string nameRegEx = '^[a-zA-Z][a-zA-Z0-9 ]+$';
private string emailRegEx = '^[a-zA-Z0-9._|\\\\%#~`=?&/$^*!}{+-]+#[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,4}$';
private string phoneRegEx = '^(\\d{3}\\-\\d{3}-\\d{4})?$';
public static Boolean Validate (String str, String validation)
{
Boolean IsValid = false;
Pattern Pattern = Pattern.compile(validation);
Matcher Matcher = Pattern.matcher(str);
if (Matcher.matches())
{
IsValid = true;
}
return IsValid;
}
Also, this function returns a Boolean. When checking its return value, you don't need the equality operators. Just use if (Validate(name, nameRegex)) { ... } or if (!Validate(email, emailRegex)) { ... }

How to implement global filter in a primefaces datatable using lazy loading?

I have an implementation of lazyDataModel following this tutorial http://uaihebert.com/?p=1120
My code its a bit different from this tutorial, here it is:
View:
<p:dataTable id="tablaClientes" value="#{AgendaManualMBean.allClientes}"
var="tablaClientes"
widgetVar="clienteTable"
rowKey="#{tablaClientes.clDocid}"
selection="#{AgendaManualMBean.ciatt001}" selectionMode="single" rows="10"
lazy="true" paginatorPosition="top"
paginatorTemplate="{RowsPerPageDropdown}{FirstPageLink}{PreviousPageLink}
{CurrentPageReport} {NextPageLink} {LastPageLink}" rowsPerPageTemplate="5,10,15"
emptyMessage="No existen clientes">
<f:facet name="header" >
<p:outputPanel>
<h:outputText value="Busqueda " />
<p:inputText id="globalFilter" onkeyup="clienteTable.filter()"
style="width:150px" size="10"/>
</p:outputPanel>
</f:facet>
<p:column id="numOrdenColumn" filterBy="#{tablaClientes.clDocid}"
width="50"
headerText="Identificación"
filterMatchMode="contains">
<h:outputText value="#{tablaClientes.clDocid}" />
</p:column>
<p:column id="nomCliColumn"
filterBy="#{tablaClientes.clNombre1}"
width="250"
headerText="Cliente"
filterMatchMode="contains">
<h:outputText value="#{tablaClientes.clNombre1}" />
</p:column>
</p:dataTable>
MY Managed Bean:
public LazyDataModel<Ciatt001> getAllClientes() {
if (listaClientesLazy == null) {
listaClientesLazy = new LazyClienteModel(ambiente.getCodCia(),ambiente.getCodAge());
//listaClientesLazy = new LazyClienteModelMBean();
}
return listaClientesLazy;
}
My LazyDataModel
public List<Ciatt001> load(int startingAt, int maxPerPage, String sortField, SortOrder sortOrder, Map<String, String> filters) {
String a = "";
try {
listaClientes = new ArrayList<Ciatt001>();
a = String.valueOf(agendamientoSession.countClientes2(cia, age));
listaClientes = agendamientoSession.listaClientes2(cia, age, startingAt, maxPerPage);
} catch (Exception e) {
e.printStackTrace();
}
if (getRowCount() <= 0) {
setRowCount(Integer.parseInt(a));
}
setPageSize(maxPerPage);
return listaClientes;
}
#Override
public Object getRowKey(Ciatt001 ciatt001) {
return ciatt001.getClDocid();
}
#Override
public Ciatt001 getRowData(String docid) {
//Integer id = Integer.valueOf(idBandeja);
for (Ciatt001 ciatt001 : listaClientes) {
if (docid.equals(ciatt001.getClDocid())) {
return ciatt001;
}
}
return null;
}
And the ejb methods:
public List<Ciatt001> listaClientes2(String cia, String age ,int startingAt, int maxPerPage) {
Query query = em.createNamedQuery("Ciatt001.listaClientesPorCiaPorAge2");
query.setParameter(1, cia);
query.setParameter(2, age);
query.setFirstResult(startingAt);
query.setMaxResults(maxPerPage);
return query.getResultList();
}
public String countClientes2(String cia, String age) {
Query query = em.createNamedQuery("Ciatt001.countClientes2");
query.setParameter(1, cia);
query.setParameter(2, age);
return query.getSingleResult().toString();
}
How can I achieve a global filter using this lazy loading implementation?
On load functon, get the filter value (check if is not null).
String filterValue = filters.get("globalFilter");
Then if you are not using any others filters ,make a query using disjunction (OR):
"select * from table where x = ? OR y = ?" //replace ? for globalFilter value
If you are using others fields, you should do:
//normal filters
filtersCondition = "(x = ? AND y = ?)" //replace ? for filters values
//global filters
globalFilterCondition = "(x = globalFilter OR y = globalFilter)" //replace ? for globalFilter value
//normal filters + global filter
query = "select * from table where " +filtersCondition+ " AND "+ globalFilterCondition
Of course, this queries are just an example , you should build a nice one and well parameterized =)
This how i get it to work , pagination and filtering :D
The LazyDataModel look like this now:
in the load method:
Set set = filters.entrySet();
Iterator i = set.iterator();
if (i.hasNext()) {
Map.Entry me = (Map.Entry) i.next();
filterValue = (String) me.getValue();
}
a = String.valueOf(agendamientoSession.countClientes2(cia, age, filterValue));
listaClientes = agendamientoSession.listaClientes2(cia, age, startingAt, maxPerPage, filterValue);
setRowCount(Integer.parseInt(a));
the ejb :
public List<Ciatt001> listaClientes2(String cia, String age ,int startingAt, int maxPerPage,String filtro) {
Query query = em.createNamedQuery("Ciatt001.listaClientesPorCiaPorAge2");
query.setParameter(1, cia);
query.setParameter(2, age);
query.setParameter(3,"%"+filtro+"%");
query.setParameter(4,"%"+filtro+"%");
query.setFirstResult(startingAt);
query.setMaxResults(maxPerPage);
}
and my query:
select * from ciatt001 c where c.cl_co_cia=? and c.cl_ag_agencia =?
and c.cl_estado='A' and (c.cl_docid like ? or c.cl_nombre1 like ? .. more filters if needed)
thats all,
now wil be looking for the sort implementation :D

MVC3 Razor VS2010 ReportViewer example

Can anyone point me to a working example of showing a report in mvc3 ie the full code, I have looked at some examples and they look like they are going to do the job but then just blow up, I know that a ASPX page is needed and there seems to be a good example at How can I use a reportviewer control in an asp.net mvc 3 razor view?
when I created a clean app i get Microsoft JScript runtime error: 'Microsoft' is undefined I know it is probably something simple but its been a while since I used Rdlc files so somthing showing a simple report populated via an xml datasource would be great, Thanks in advance
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 t
o 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&parameters=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;
}
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.

Complete solution to Combine, Minify and GZIP your Styles AND Scripts on Asp.Net MVC3 using Razor

Sorry for my bad English, but i guess it won't be a problem. I just wan't to share a nice Helper Class that i made to combine, minify and gzip our scripts and styles using the Microsoft Ajax Minifier. Before start, download the ICSharpCode.SharpZipLib. This is an open source lib to use gzip.
Let's start by the web.config (i'll focus at IIS7). Here we are saying to our application that any request made to cssh or jsh extensions, will be forwarded to the class MinifierHelper. I chosen to use these extensions (cssh and jsh) to if we want, for any reason, don't minify a specific script or style, use it the way you use normally.
<system.webServer>
<handlers>
<remove name="ScriptHandler" />
<remove name="StyleHandler" />
<add name="ScriptHandler" verb="*" path="*.jsh" type="Site.Helpers.MinifierHelper" resourceType="Unspecified" />
<add name="StyleHandler" verb="*" path="*.cssh" type="Site.Helpers.MinifierHelper" resourceType="Unspecified" />
</handlers>
</system.webServer>
I use the folders Scripts and Styles to store the files. I don't use the folder Content as suggested by Visual Studio.
The next step is to configure global.asax. We have to tell our application to not route these folders. Add these lines to your RegisterRoutes Method.
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("Scripts/{*path}");
routes.IgnoreRoute("Styles/{*path}");
...
}
OK. Now i'll show how to use our class. In the View:
<link href="/Styles/Folder/File.cssh" type="text/css" rel="stylesheet" />
<script src="/Scripts/Folder/File.jsh" type="text/javascript"></script>
In my example, i made the logic to all my scripts and styles to be inside folders inside the Scripts/Styles. Ex: Site -> Scripts -> Home -> index.css. I use the same structure used to the Views to the Scripts and Styles. Ex: Site -> Views -> Home -> index.cshtml. You can change this pattern if you want.
Now the code to make the magic:
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Caching;
using System.Web.SessionState;
using ICSharpCode.SharpZipLib.GZip;
using Microsoft.Ajax.Utilities;
namespace Site.Helpers
{
public abstract class MinifierBase : IHttpHandler, IRequiresSessionState
{
#region Fields
private HttpContext context;
private byte[] responseBytes;
private bool isScript;
protected string fileName;
protected string folderName;
protected List<string> files;
#endregion
#region Properties
public bool IsReusable
{
get { return false; }
}
#endregion
#region Methods
public static string setUrl(string url)
{
var publishDate = ConfigurationManager.AppSettings["PublishDate"];
return url + "h" + ((publishDate != null) ? "?id=" + publishDate : "");
}
public void ProcessRequest(HttpContext context)
{
this.context = context;
this.isScript = context.Request.Url.PathAndQuery.Contains("/Scripts/");
this.process();
}
private void process()
{
if (this.context.Request.QueryString.HasKeys())
{
string url = this.context.Request.Url.PathAndQuery;
if (this.context.Cache[url] != null)
{
this.responseBytes = this.context.Cache[url] as byte[];
}
else
{
this.writeResponseBytes();
this.context.Cache.Add
(
url,
this.responseBytes,
null,
DateTime.Now.AddMonths(1),
Cache.NoSlidingExpiration,
CacheItemPriority.Low,
null
);
}
}
else
{
this.writeResponseBytes();
}
this.writeBytes();
}
private void writeResponseBytes()
{
using (MemoryStream ms = new MemoryStream(8092))
{
using (Stream writer = this.canGZip() ? (Stream)(new GZipOutputStream(ms)) : ms)
{
var sb = new StringBuilder();
var regex = new Regex(#"^/.+/(?<folder>.+)/(?<name>.+)\..+");
var url = regex.Match(this.context.Request.Path);
var folderName = url.Groups["folder"].Value;
var fileName = url.Groups["name"].Value;
this.getFileNames(fileName, folderName).ForEach(delegate(string file)
{
sb.Append(File.ReadAllText(this.context.Server.MapPath(file)));
});
var minifier = new Minifier();
var minified = string.Empty;
if (this.isScript)
{
var settings = new CodeSettings();
settings.LocalRenaming = LocalRenaming.CrunchAll;
settings.OutputMode = OutputMode.SingleLine;
settings.PreserveImportantComments = false;
settings.TermSemicolons = true;
minified = minifier.MinifyJavaScript(sb.ToString(), settings);
}
else
{
var settings = new CssSettings();
settings.CommentMode = CssComment.Important;
settings.OutputMode = OutputMode.SingleLine;
minified = minifier.MinifyStyleSheet(sb.ToString(), settings);
}
var bts = Encoding.UTF8.GetBytes(minified);
writer.Write(bts, 0, bts.Length);
}
this.responseBytes = ms.ToArray();
}
}
private List<String> getFileNames(string fileName, string folderName = "")
{
this.files = new List<String>();
this.fileName = fileName;
this.folderName = folderName;
if (folderName == "Global" && fileName == "global-min")
{
if (this.isScript) this.addGlobalScripts();
else this.addDefaultStyles();
}
else
{
var flags = BindingFlags.NonPublic | BindingFlags.InvokeMethod | BindingFlags.Instance;
var mi = this.GetType().GetMethod
(
"add" +
this.folderName +
CultureInfo.CurrentCulture.TextInfo.ToTitleCase(fileName).Replace("-", "") +
(this.isScript ? "Scripts" : "Styles"),
flags
);
if (mi != null)
{
mi.Invoke(this, null);
}
else
{
if (this.isScript) this.addDefaultScripts();
else this.addDefaultStyles();
}
}
return files;
}
private void writeBytes()
{
var response = this.context.Response;
response.AppendHeader("Content-Length", this.responseBytes.Length.ToString());
response.ContentType = this.isScript ? "text/javascript" : "text/css";
if (this.canGZip())
{
response.AppendHeader("Content-Encoding", "gzip");
}
else
{
response.AppendHeader("Content-Encoding", "utf-8");
}
response.ContentEncoding = Encoding.Unicode;
response.OutputStream.Write(this.responseBytes, 0, this.responseBytes.Length);
response.Flush();
}
private bool canGZip()
{
string acceptEncoding = this.context.Request.Headers["Accept-Encoding"];
return (!string.IsNullOrEmpty(acceptEncoding) && (acceptEncoding.Contains("gzip") || acceptEncoding.Contains("deflate")));
}
protected abstract void addGlobalScripts();
protected abstract void addGlobalStyles();
protected abstract void addDefaultScripts();
protected abstract void addDefaultStyles();
#endregion
}
That's the base class. Now we will create the Helper class that inherits from the base class. That's the class where we choose which scripts should be added.
public class MinifierHelper : MinifierBase
{
#region Methods
To combine and minify global scripts/styles add the current line to your View:
<link href="#MinifierHelper.setUrl("/Styles/Global/global-min.css")" type="text/css" rel="stylesheet" />
<script src="#MinifierHelper.setUrl("/Scripts/Global/global-min.js")" type="text/javascript"></script>
It'll invoke the methods addGlobalScripts/addGlobalStyles in our MinifierHelper class.
protected override void addGlobalScripts()
{
this.files.Add("~/Scripts/Lib/jquery-1.6.2.js");
this.files.Add("~/Scripts/Lib/jquery-ui-1.8.16.js");
this.files.Add("~/Scripts/Lib/jquery.unobtrusive-ajax.js");
this.files.Add("~/Scripts/Lib/jquery.validate.js");
...
}
protected override void addGlobalStyles()
{
this.files.Add("~/Styles/Global/reset.css");
this.files.Add("~/Styles/Global/main.css");
this.files.Add("~/Styles/Global/form.css");
...
}
To minify specific script/style (specific to the page) add the current line to your View:
<link href="#MinifierHelper.setUrl("/Styles/Curriculum/index.css")" type="text/css" rel="stylesheet" />
The MinifierHelper class will try to find a method with the name "add" + FolderName + FileName + "Styles". In our case it will look for addCurriculumIndexStyles. In my example it exists, so the method will be triggered.
public void addCurriculumIndexStyles()
{
this.files.Add("~/Styles/Global/curriculum.css");
this.files.Add("~/Styles/Curriculum/personal-info.css");
this.files.Add("~/Styles/Curriculum/academic-info.css");
this.files.Add("~/Styles/Curriculum/professional-info.css");
}
If the class don't find that specific method, it'll trigger the default method. The default method minify a script/style using the same folder/name specified.
protected override void addDefaultScripts()
{
this.files.Add("~/Scripts/" + this.folderName + "/" + this.fileName + ".js");
}
protected override void addDefaultStyles()
{
this.files.Add("~/Styles/" + this.folderName + "/" + this.fileName + ".css");
}
Don't forget to close the region and class.
#endregion
}
Thats it. I hope you guys have understood.
I forgot to tell one last thing. In the web.config add a key in AppSettings with the name PublishDate. I put in the value a string with the full date and time (ex: 261020111245). The goal is to be unique. This key will be used to cache our minified scripts. If you don't create this key, your application won't use cache. I recommend to use this. So everytime you update your scripts/styles, update your PublishDate also.
<add key="PublishDate" value="261020111245" />
The Mindscape Web Workbench is a great tool for doing the items you are looking for.
http://visualstudiogallery.msdn.microsoft.com/2b96d16a-c986-4501-8f97-8008f9db141a
Here is a good blog post about it:
http://visualstudiogallery.msdn.microsoft.com/2b96d16a-c986-4501-8f97-8008f9db141a

ICEfaces multiple selectInputText widgets inside datatable behave erratically

I have multiple selectInputText controls inside a datatable, something like this:
<ice:dataTable id="attributesList" value="#{myForm.myAttributes}" var="entry" cellpadding="0" rows="9999" columnClasses="myColumn,myColumn">
<ice:column>
<!-- auto-complete -->
<ice:panelGroup>
<ice:selectInputText rows="15" width="120" maxlength="255" value="#{entry.attribute.stringValue}" valueChangeListener="#{myFieldAutocomplete.updateList}" immediate="true">
<f:selectItems value="#{myFieldAutocomplete.list}" />
<f:attribute name="fieldName" value="#{entry.name}" />
</ice:selectInputText>
</ice:panelGroup>
</ice:column>
</ice:dataTable>
My backing bean code is this:
public class myFieldAutocomplete {
// default value, empty string
private String currentFieldValue = "";
// list of possible matches.
private List<SelectItem> matchesSIList = new ArrayList<SelectItem>();
public void updateList(ValueChangeEvent event) {
currentFieldValue = "";
// get a new list of matches.
setMatches(event);
if (event.getComponent() instanceof SelectInputText) {
SelectInputText autoComplete = (SelectInputText) event.getComponent();
if (autoComplete.getSelectedItem() != null) {
currentFieldValue = (String) autoComplete.getSelectedItem().getValue();
}
else {
String tempValue = getMatch(autoComplete.getValue().toString());
if(tempValue != null) {
currentFieldValue = tempValue;
}
}
}
}
public String getCurrentFieldValue() {
return currentFieldValue;
}
public List<SelectItem> getList() {
return matchesSIList;
}
private String getMatch(String value) {
String result = null;
if (matchesSIList != null) {
String str;
Iterator<SelectItem> itr = matchesSIList.iterator();
while (itr.hasNext()) {
SelectItem si = (SelectItem) itr.next();
str = (String) si.getValue();
if (str.startsWith(value)) {
result = str;
break;
}
}
}
return result;
}
public void setMatches(ValueChangeEvent event) {
List<String> newMatchesStrList = new ArrayList<String>();
if (event.getComponent() instanceof SelectInputText) {
SelectInputText autoComplete = (SelectInputText) event.getComponent();
myClassDAO myDao = (myClassDAO) Context.getInstance().getBean(myClassDAO.class);
String fieldName = (String) autoComplete.getAttributes().get("fieldName");
newMatchesStrList = myDao.findTemplateFieldValues((String)autoComplete.getValue(), fieldName);
}
// assign new matchList
if (this.matchesSIList != null) {
this.matchesSIList.clear();
}
Iterator<String> itr = newMatchesStrList.iterator();
while(itr.hasNext()) {
String str = (String) itr.next();
matchesSIList.add(new SelectItem(str, str));
}
}
}
The bean is in request scope (although I also tried with session scope). I am using ICEfaces community edition 1.8.2.
The problem is, these auto-complete controls are created dynamically based on certain attributes of each entry in the datatable. So, you can have, for example 2 or more such auto-complete controls in the same panelGroup. When this is the case, when you start typing something the first control, it seems to be triggering the event for all sibling auto-complete controls, and in the end returns the list for the last in order.
In general, I noticed erratic behavior caused by the fact that the event is triggered for all auto-complete controls at once, and values/lists get confused.
What am I doing wrong?
Thanks in advance!

Resources