------------------------------------------------------------
revno: 1147
committer: Roger Martin <[email protected]>
branch nick: aikiframework
timestamp: Sat 2012-03-17 08:56:19 +0100
message:
working on forms
modified:
libs/Controls.php
libs/Engine_v2.php
libs/markup.php
--
lp:aikiframework
https://code.launchpad.net/~aikiframework-devel/aikiframework/trunk
Your team Aiki Framework Developers is subscribed to branch lp:aikiframework.
To unsubscribe from this branch go to
https://code.launchpad.net/~aikiframework-devel/aikiframework/trunk/+edit-subscription
=== modified file 'libs/Controls.php'
--- libs/Controls.php 2012-03-09 22:23:07 +0000
+++ libs/Controls.php 2012-03-17 07:56:19 +0000
@@ -17,15 +17,38 @@
* @filesource
*/
-/*if (!defined('IN_AIKI')) {
+if (!defined('IN_AIKI')) {
die('No direct script access allowed');
-}*/
+}
//@TODO MOVE TO AIKI LIBRARY
-function bydefault( &$array, $key, $value) {
- return isset($array[$key]) ? $array[$key]: $value;
-}
+function bydefault( &$array, $key, $ifnotfound=NULL) {
+ return is_array($array) && isset($array[$key]) ? $array[$key]: $ifnotfound;
+}
+
+function htmlAttributes( &$field, $moreAttributes ){
+
+ $allAttributes= array(
+ "class","onchange","onfocus","onblur" );
+
+ if ( !is_null($moreAttributes ) ){
+ $allAttributes = array_merge($allAttributes, explode(" ",$moreAttributes));
+ }
+
+ $ret="";
+ foreach ($allAttributes as $attribute ){
+ if ( isset($field[$attribute]) ){
+ if ( $attribute=="checked" || $attribute=="disabled" ){
+ $ret .= " $attribute";
+ } else {
+ $ret .= " $attribute='". addslashes($field[$attribute]). "'";
+ }
+ }
+ }
+ return $ret;
+}
+
@@ -34,10 +57,11 @@
*/
-class Forms2 {
+class Forms2 { //NOTE: aiki old forms have this class
private $defaults;
-
-
+
+ public $last_error;
+
function __construct ( $parameters ){
// here are the default parameters
$defaults = array (
@@ -45,22 +69,61 @@
"action" => "?",
"id" => uniqid("f_"), // by default a random unique
"http" => "http", // @todo current protocol
- "layout" => "table"
+ "layout" => "table",
+ "submit" => t("submit"),
+ "enctype"=> "application/x-www-form-urlencoded "
);
+ $this->last_error ="";
// read new parameters.
if ( is_array($parameters) ){
$this->defaults = $parameters + $defaults;
}
- }
-
- /**
- *
+
+ }
+
+
+ /**
+ *
+ * change the enctype to send files
+ *
+ */
+
+ function set_for_files(){
+ $this->defaults["enctype"]= "multipart/form-data";
+ }
+
+ function message ( $text, $type="ok") {
+ return "<div class='form-message form-message-$type'>$text</div>";
+ }
+
+ /**
+ *
* Returns the HTML code for the form inserting the values
- *
- */
+ *
+ */
+
+ function render(){
+ // Process the form
+ $message ="";
+ if ( $this->is_set("save") ){
+ if ( isset($_POST[$this->defaults["save"]]) ){
+ if ( !$this->save_data() ) {
+ $message = $this->message)
+ "Error saving/adding record " . ( is_debug_on() ? $this->last_error : "" ),
+ "error");
+ } elseif ( $this->is_set("pkey") ){
+ $message = $this->message( "Record saved", "ok form-record-saved"),
+ } else {
+ $message = $this->message( "Record added", "ok form-record-add"),
+ }
+ }
+ // to avoid re-write the form
+ $this->defaults["method"]="post";
+ }
- function render($values=NULL){
+
+ // RENDER
$attributes="";
// here we insert form attributes
foreach ( array("class","id","action","method", "enctype", "onsubmit", "onreset") as $attribute ){
@@ -68,36 +131,200 @@
$attributes .= " $attribute='". addslashes($this->defaults[$attribute]). "'" ;
}
}
-
- // yes, there are people who just wants a blank forms!
+
+ // yes, there are people who just wants a blank forms!
if ( !isset($this->defaults["fields"]) || !is_array($this->defaults["fields"] ) ){
return "<form$attributes></form>\n";
- }
-
- return
+ }
+
+ $values= $this->load_data();
+
+ // generate the default buttons submit and reset.
+ $buttons="";
+ if ( $this->is_set("submit") ) {
+ $temp = Controls::escape_value($this->defaults["submit"]);
+ $buttons = "<input type='submit' name='submit' value='$temp' id='{$this->defaults['id']}-submit'>";
+ }
+
+ if ( $this->is_set("reset") ) {
+ $temp = Controls::escape_value($this->defaults["reset"]);
+ $buttons .= "<input type='reset' name='reset' value='$temp' id='{$this->defaults['id']}-reset'>";
+ }
+
+ if ($buttons){
+ $buttons= "<div id='{$this->defaults['id']}-buttons'>$buttons</div>";
+ }
+
+ return
"<form$attributes>\n".
+ $message.
Controls::inputs($this->defaults["fields"],$this,$values ).
- "</form>";
+ $buttons.
+ "</form>";
}
-
-
+
+
/**
- *
+ *
* Magic method to get forms parameters
- *
- */
+ *
+ */
function __get($what){
return isset($this->defaults[$what]) ? $this->defaults[$what]: NULL ;
}
-
+
+ function is_set($key){
+ return isset($this->defaults[$key]) && $this->defaults[$key];
+ }
+
+ function load_data(){
+ global $db;
+
+ //obtain default values using ['default'] key
+ $values= array();
+ if ( $this->is_set("fields") && is_array($this->defaults["fields"]) ){
+ foreach ($this->defaults["fields"] as $field){
+ if ( isset($field["default"]) && isset($field["name"]) ){
+ $values[ $field["name"] ] = $field["default"];
+ }
+ }
+ }
+
+ // now try to make a query using ['SQL']
+ if ( $this->is_set("pkey") && $this->is_set("SQL") ) {
+ $SQL = str_replace('$PKEY', $this->defaults["pkey"], $this->defaults["SQL"]);
+ } elseif ( $this->is_set("pkeyfield") && $this->is_set("pkey") && $this->is_set("table") ){
+ // or table, pkeyfield...
+ $SQL = sprintf (
+ "SELECT * FROM %s WHERE %s='%s'",
+ $this->defaults["table"],
+ $this->defaults["pkeyfield"],
+ $this->defaults["pkey"]);
+ } elseif ( $this->is_set("fields") && is_array($this->defaults["fields"]) ){
+ return $values;
+ }
+
+ // @TODO WHAT DO WITH error's
+ $db->last_error = "";
+ $news = $db->get_row($SQL,ARRAY_A);
+ if ( $db->last_error ){
+ $this->last_error = $db->last_error;
+ return $values;
+ }
+
+ // mix values
+ return $news + $values;
+ }
+
+ function is_adequate_type($field){
+ // check if there is a name
+ if ( !isset($field['name']) || trim($field['name'])=="" ||
+ !isset($field['type']) || !$field['type']){
+ return false;
+ }
+
+ // check if there is a name
+ switch ($field['type']){
+ case 'submit':
+ case 'reset' :
+ return false;
+ default:
+ return true;
+ }
+
+ }
+
+
+ function get_field_value($fieldname){
+
+ /*if ( $this->defaults["method"] == "get" ) {
+ $ret= isset( $_GET[$fieldname]) ? $_GET[$fieldname] : NULL;
+ } else {
+ $ret= isset( $_POST[$fieldname]) ? $_POST[$fieldname] : NULL;
+ }*/
+
+ $ret= isset( $_POST[$fieldname]) ? $_POST[$fieldname] :
+ ( isset( $_GET[$fieldname]) ? $_GET[$fieldname] : NULL);
+ return ( is_null($ret) ? NULL: "'". Controls::unescape_textarea($ret). "'");
+ }
+
+ function save_data(){
+ global $db;
+
+ $this->last_error="";
+
+ if ( !$this->is_set("table") ){
+ $this->last_error = t("Missing table");
+ return false;
+ }elseif ( !$this->is_set("fields") || !is_array ($this->defaults["fields"])){
+ $this->last_error = t("Missing fields definition");
+ return false;
+ } elseif ( $this->is_set("pkey") && !$this->is_set("pkeyfield") ){
+ $this->last_error = t("Missing key field name");
+ return false;
+
+ } elseif ( $this->is_set("pkey")) {
+ // is a update for a existing record
+ $pairs=array();
+ foreach ( $this->defaults["fields"] as $field ){
+ if ( !$this->is_adequate_type($field)) {
+ continue;
+ }
+ $value= $this->get_field_value($field['name']);
+ //note: get_field_value sorround all values with quotes
+ if ( !is_null($value) ){
+ $pairs[]= "{$field['name']}=$value";
+ }
+ }
+ if ( count($pairs)==0){
+ $this->last_error = t("No values recollected");
+ return false;
+ }
+ $SQL= "UPDATE {$this->defaults['table']} SET ".
+ implode(",", $pairs).
+ " WHERE {$this->defaults['pkeyfield']}={$this->defaults['pkey']}";
+
+ } else {
+ // adding a new record
+ $fields = array();
+ $values = array();
+ foreach ( $this->defaults["fields"] as $field ){
+ if ( !$this->is_adequate_type($field)) {
+ continue;
+ }
+ $value= $this->get_field_value($field['name']);
+ if ( !is_null($value) ){
+ $sets[] = $field["name"];
+ $values[]= $value;
+ }
+ }
+ if (count($fields)==0){
+ $this->last_error = t("No values recollected");
+ return false;
+ }
+ $SQL= "INSERT {$this->defaults['table']} (".
+ implode (",", $sets ) . ") VALUES ( ".
+ implode (", , $values) .");
+ }
+ $db->last_error = "";
+ $db->query($SQL);
+ if ( $db->last_error ){
+ echo $SQL,"<br>";
+ $this->last_error = $db->last_error;
+ return false;
+ }
+ return true;
+
+ }
+
}
/**
- *
+ *
* Class for controls: input, select, textarea
- *
- */
+ *
+ */
class Controls {
@@ -107,7 +334,7 @@
if ( is_array($list) ) {
return $list;
}
-
+
if ( substr($list,0,4)=="SQL:" ){
$ret= array();
if ($results= $db->query( substr($list,5), N_ARRAY )){
@@ -120,171 +347,221 @@
return array("Male", "Female");
} elseif ( $list=="yn"){
return array("Yes", "No");
- }
- return array( explode("|",$list) );
+ }
+ return explode("|",$list);
}
-
-
+
+
/**
- *
+ *
* Return html code for a input hidden
- *
- */
-
- function input_hidden($field,$values=NULL){
+ *
+ */
+
+ function input_hidden($field,$value){
$name = $field['name'];
- if (is_null($values) ){
- $value = isset($field["value"]) ? $field["value"] : "";
- } elseif (isset($values['value'])) {
- $value = $values["value"];
- } elseif (isset($fields['value'])) {
- $value = $fields['value'];
- } else {
- $value="";
+ $value= addslashes($value);
+ return "<input type='hidden' name='{$field['name']}' value='$value'>";
+ }
+
+ function escape_value($value){
+ return htmlentities($value, ENT_QUOTES);
+ }
+
+ function escape_textarea($value){
+ return strtr(
+ $value,
+ array("<textarea"=>"<text-area", "</textarea>"=>"</text-area>"));
+ }
+
+ function unescape_textarea($value){
+ return strtr(
+ $value,
+ array("<text-area"=>"<textarea", "</text-area>"=>"</textarea>"));
+ }
+
+ function additional( &$field) {
+ $ret = "";
+ foreach ( array("additional","tooltip" ) as $more ){
+ if ($add = bydefault($field, $more , false) ){
+ $ret .= "<span class='$more'>$add</span>";
+ }
}
- return "<input type='hidden' name='{$field['name']}' value='". addslashes($value)."'>";
+ return $ret;
}
-
-
+
/**
- *
+ *
* Return html code for generic input
- *
- */
-
+ *
+ */
+
function input ( $field, $form=NULL, $values=NULL ) {
- if ( is_null($form) ){
- $layout = bydefault($field,'layout','table');
- $formID = bydefault($field,'form',"aiki_form");
- } else {
- $layout = isset($form->layout) ? $form->layout :"table";
- $formID = $form->id;
- }
-
-
- $name = bydefault($field,'name',false);
- $type = bydefault($field,'type',"text");
- $label = bydefault($field,'label',$name);
- $more = bydefault($field,'more' ,"");
-
-
- // first, discard hidden, and find field label
- switch ( $type ) {
- case "hidden":
- return Control::input_hidden($field,$values);
- case "radios":
- $label = "<span class='label'>$label</span>";
- break;
- default:
- $label = "<label for='{$formID}_$name'>$label</label>";
- }
-
- // now the hard work, input;
- $input="";
-
- $fieldValue = is_null($values) || !isset($values[$name]) ? NULL: $values[$name];
- $more = $more ? "<span class='more'>$more</span>":"";
-
-
- switch ( $type ){
- case "radios":
- $list = Controls::make_list($field["list"]);
- foreach ($list as $key=>$value){
- if ( $fieldValue && $key==$fieldValue) {
- $checked=" checked='checked'";
- } else {
- $checked="";
- }
- $input .=
- "<input type='radio' value='$key' name='$name' id='{$formID}_$name_$key'$checked>".
- "<label for='{$formID}_$name'>$value</label>";
- }
- break;
-
- case "select":
- $list = Controls::make_list($field["list"]) ;
- foreach ($list as $key=>$value){
- if ( $fieldValue && $key==$fieldValue) {
- $selected=" selected='selected'";
- } else {
- $selected="";
- }
- $input .="\n<option value='$key'$selected>$value</option>";
- }
- $input = "<select name='$name' id='{$formID}_$name'>$input\n</select>";
- break;
-
- case "textarea":
- $cols = bydefault($field,'cols',60);
- $rows = bydefault($field,'rows',10);
- $input = sprintf(
- "<textarea name='$name' id='{$formID}_$name' rows='$rows' cols='$cols' >%s</textarea>",
- bydefault($values,$name,"") );
- break;
-
- case "file" : //@TODO set form enctype.
- case "text":
- case "password":
- $length = bydefault($field,'maxlength',0);
- if ( $type=="string" && $fieldValue){
- $attribValue= " value='" . addslashes($fieldValue)."'";
- } else {
- $attribValue="";
- }
- $input = "<input type='$type' name='$name' id='{$formID}_$name'$attribValue>";
- break;
- //@TODO add HTML5 inputs.
- }
-
- $input .= $more;
- // final layout of values.
- $layouts= array(
- "table" => "\n<tr><th>\$label</th><td>\$input</td></tr>",
- "none" => "\$label \$input",
- "p-br" => "\n<p>\$label<br>\$input</p>",
- "p" => "\n<p>\$label \$input</p>",
- "dl" => "\n<dt>\$label</dt><dd>\$input</dd>");
- $useLayout = isset($layouts[$layout]) ? $layouts[$layout] : $layout;
-
- return strtr($useLayout, array('$label'=>$label,'$input'=>$input));
-
- }
-
-
+ //HTML5 tag.
+ $defined = array (
+ "button" =>"value",
+ "checkbox" =>"checked disabled value",
+ "color" =>"value",
+ "date" =>"value",
+ "datetime" =>"value",
+ "datetime-local" =>"value",
+ "email" =>"value",
+ "file" =>"",
+ "hidden" =>"value",
+ "image" =>"value src",
+ "month" =>"value",
+ "number" =>"value min max step",
+ "password" =>"value",
+ "radio" =>"checked disabled value",
+ "range" =>"",
+ "reset" =>"value",
+ "search" =>"value",
+ "submit" =>"value",
+ "tel" =>"value",
+ "text" =>"value size maxlength pattern placeholder",
+ "time" =>"value",
+ "url" =>"value",
+ "week" =>"value"
+ );
+
+
+ if ( is_null($form) ){
+ $layout = bydefault($field,'layout','table');
+ $formID = bydefault($field,'form',"aiki_form");
+ } else {
+ $layout = is_null($form->layout) ? 'table' : $form->layout;
+ $formID = $form->id;
+ }
+
+ $name = bydefault($field,'name',false);
+ $type = bydefault($field,'type',"text");
+ $label = bydefault($field,'label',$name);
+ $value = ( !$name ?
+ byDefault($field,"default",""):
+ byDefault($values,$name,"") );
+
+ // first, discard hidden, and find field label
+ switch ( $type ) {
+ case "hidden":
+ return Controls::input_hidden($field,$value);
+ case "radios":
+ $label = "<span class='label'>$label</span>";
+ break;
+ default:
+ $label = "<label for='{$formID}_$name'>$label</label>";
+ }
+
+ // now the hard work, input;
+ $input="";
+
+ $attributes = htmlAttributes( $field, isset($defined[$type]) ? $defined[$type] : NULL );
+
+ switch ( $type ){
+ case "radios":
+ $list = Controls::make_list($field["list"]);
+ foreach ($list as $key=>$literal){
+ if ( $value && $key==$value) {
+ $checked =" checked='checked'";
+ } else {
+ $checked ="";
+ }
+ $temp = addslashes($key);
+ $input .=
+ "<input type='radio' value='$temp' name='$name' id='{$formID}_{$name}_{$key}'$checked $attributes>".
+ "<label for='{$formID}_{$name}'>{$literal}</label>";
+ }
+ break;
+
+ case "select":
+ $list = Controls::make_list($field["list"]) ;
+ foreach ($list as $key=>$option) {
+ if ( $value && $key==$value) {
+ $selected = " selected='selected'";
+ } else {
+ $selected = "";
+ }
+ $temp = addslashes($key);
+ $input .="\n<option value='$temp'$selected>$option</option>";
+ }
+ $input = "<select name='$name' id='{$formID}_$name' $attributes>$input\n</select>";
+ break;
+
+ case "textarea":
+ $cols = bydefault($field,'cols',60);
+ $rows = bydefault($field,'rows',10);
+ $input = sprintf(
+ "<textarea name='$name' id='{$formID}_$name' rows='$rows' cols='$cols' $attributes>%s</textarea>",
+ Controls::escape_textarea($value));
+
+ break;
+
+ case "file" :
+ if ( isset($form) ){
+ $form->set_for_files();
+ }
+ // no break
+ default:
+ $attributes = htmlAttributes( $field, isset($defined[$type]) ? $defined[$type] : NULL );
+ $temp = Controls::escape_value($value);
+ $input = "<input type='$type' value='$temp' name='$name' id='{$formID}_$name'$attributes>";
+ break;
+ //@TODO add HTML5 inputs.
+ }
+
+ $input .= Controls::additional($field);
+ // final layout of values.
+ $layouts= array(
+ "table" => "\n<tr><th>\$label</th><td>\$input</td></tr>",
+ "none" => "\$label \$input",
+ "p-br" => "\n<p>\$label<br>\$input</p>",
+ "p" => "\n<p>\$label \$input</p>",
+ "dl" => "\n<dt>\$label</dt><dd>\$input</dd>");
+
+ $useLayout = isset($layouts[$layout]) ? $layouts[$layout] : $layout;
+
+ return strtr($useLayout, array('$label'=>$label,'$input'=>$input));
+
+ }
+
+
/**
- *
+ *
* Return html code for a list of input
- *
- */
+ *
+ */
function inputs( &$inputs, $form, &$values){
$hidden = "";
$ret = "" ;
foreach($inputs as $input) {
- //hidden values
- switch ($input["type"]){
- case "hidden":
- $hidden .= Controls::input_hidden($input,$values);
- break;
+ if ( !is_array($input) ){
+ continue;
+ }
+
+ //hidden values
+ switch ($input["type"]){
case "html":
$ret .= $input["html"];
break;
+ case "callback";
+ $ret .= call_user_func($input["html"]);
+ break;
default:
- $ret .= Controls::input($input, $form, $values);
+ $ret .= Controls::input($input, $form, $values);
}
}
-
+
$layouts= array(
"table" => "\n<table>\$inputs</table>",
"none" => "\$inputs",
"p-br" => "\n\$inputs",
"p" => "\n\$inputs",
"dl" => "\n<dl>\$inputs</dl>");
-
- $layout = isset($form->layout) ? $form->layout : 'table';
+
+ $layout = is_null($form->layout) ? 'table' : $form->layout ;
$useLayout = isset($layouts[$layout]) ? $layouts[$layout] : $layouts['table'];
-
- return $hidden.str_replace ( '$inputs', $ret, $useLayout);
+
+ return $hidden.str_replace ( '$inputs', $ret, $useLayout);
}
}
=== modified file 'libs/Engine_v2.php'
--- libs/Engine_v2.php 2012-03-09 22:23:07 +0000
+++ libs/Engine_v2.php 2012-03-17 07:56:19 +0000
@@ -200,8 +200,6 @@
$result = t("Parser $parserToCall not found");
}
-
-
return $match[0].
($match[1]=="noaiki" ? $result: $this->parse($result)).
$this->parse($match[5]);
@@ -321,17 +319,6 @@
return $aiki->AikiScript->parser($trueblock,false);
}
- /*
- * Parse if
- */
- function parse_if($condition,$trueblock,$elseblock){
- if (isset($condtion[0]) && $condition[0]) {
- return $trueblock;
- } else {
- return $elseblock ;
- }
- }
-
/**
* translation
*/
@@ -569,13 +556,13 @@
private function parse_hits($hit, $true, $else ) {
global $db;
- if ( len($hit) == 3 ){
+ if ( count($hit) == 3 ){
$db->query(
"UPDATE {$hit[0]}".
" SET {$hit[2]}={$hit[2]}+1".
" WHERE {$hit[1]}");
} elseif (is_debug_on() ) {
- return sprintf( __("BAD HITS PARAMETERS: 3 expected, %d given"), len($hit) );
+ return sprintf( t("BAD HITS PARAMETERS: 3 expected, %d given"), len($hit) );
}
return "";
}
@@ -590,7 +577,11 @@
function parse_view( $para, $true, $false){
global $aiki;
- list($view,$language)= exlode("/",$filter[1]."/*",2);
+ if ( !isset($para[0])){
+ return $false;
+ }
+
+ list($view,$language)= exlode("/",$para[0]."/*",2);
if ( match_pair_one( $view, $aiki->site->view()) &&
match_pair_one( $language, $aiki->site->language() )){
@@ -603,18 +594,14 @@
function parse_permission($para, $true, $false){
global $aiki, $db;
- if ( strpos($widget,"||") !== false ){
- list($filter,$content) = explode("||", $widget, 2);
- } else {
- return $widget;
- }
-
- /* fake permission */
- if ( trim($filter) == "user" ){
- return $content;
- }
- return "";
-
+
+ if ( !isset($para[0])) {
+ return $true;
+ }
+
+ $filter= $para[0];
+
+ // @TODO call a membership method
$sql = "SELECT group_level" .
" FROM aiki_users_groups".
" WHERE group_permissions='". addslashes($filter) ."'";
@@ -623,15 +610,15 @@
if ( trim($filter) == $aiki->membership->permissions ||
$aiki->membership->group_level < $get_group_level ) {
- return $content;
+ return $true;
}
- return "";
+ return $false;
}
/*
- * These are simple parse with only a raw block as argument.
+ * These are simple parsers with only a raw block as argument.
*/
function parse_noaiki($text){
@@ -652,17 +639,36 @@
* here are the (micro)-parser
*/
+ /*
+ * forms
+ */
+
function parse_forms( $para, $true, $false){
$formParameters = array_shift($para) ;
- $formParameters["fields"]= $para;
+ $formParameters["fields"]= $para;
$form= new Forms2($formParameters);
return $form->render();
}
+ /*
+ * controls
+ */
+
function parse_controls( $para, $true, $false){
return Controls::input($para[0]);
}
+ /*
+ * if
+ */
+ function parse_if($condition,$trueblock,$elseblock){
+ if (isset($condtion[0]) && $condition[0]) {
+ return $trueblock;
+ } else {
+ return $elseblock ;
+ }
+ }
+
=== modified file 'libs/markup.php'
--- libs/markup.php 2012-03-09 22:23:07 +0000
+++ libs/markup.php 2012-03-17 07:56:19 +0000
@@ -251,7 +251,7 @@
* 1=> " rest"
*/
-function extract_parameters( $string, $find="") {
+function extract_parameters( $string, $find="", $delimiter=',') {
$max= strlen($string);
@@ -285,7 +285,7 @@
}
break;
// do we found a other arg?
- case ',':
+ case $delimiter:
if (($state==0 || $state==3) && $level==0) {
$add = true;
$char= false;
_______________________________________________
Mailing list: https://launchpad.net/~aikiframework-devel
Post to : [email protected]
Unsubscribe : https://launchpad.net/~aikiframework-devel
More help : https://help.launchpad.net/ListHelp