State Widget ?

Hi

Would it be possible to have a kind of state widget we could manually place inside a form. You would assign it a name and when it is rendered it will request its "state" information from the controller using the assigned name. So you could do something like the following in the controller

$this->saveState('stateWidgetName','attributeName', 'value')

The view would contain

<com:StateWidget name="stateWidgetName" />

When rendered the view would request the state from the controller. On a web page the state would be a hidden field with the serialized value of the state or if cache available a cacheid. The field would be named in a way that it could be identified when it was returned, so a controller could call

$this->getState('stateWidgetName','attributeName','defaultValue')

to retrieve an object from the state.

Thoughts ?

NZ

This would be like Prado's viewstate. However, the implementation is not trivial. The main reason is that a page could contain many forms. Using hidden field to store state information would mean placing the same hidden field in each form.

Kind of - I was thinking each state widget would store specific information according to the containing forms' purpose

For example you have a page with 2 forms, one is an opinion poll (inside a form) the other is a form containing some screen to edit a database record. When the opinion poll gets submitted it does not need to know anything about the database record that is being edited so it may not have any state widget contained inside the form. But the database record form could have state information, so the developer added a state widget inside that form.

So you do not really have to add any specific logic there can only be (at most) one state inside each form. When a call is made to retrieve a value from the state the request parameters can be examined for a predefined name and the value be extracted from there, if the parameter does not exist then null is returned. Hmm so the get would actually be : $this->getState('attributeName','defaultValue')

When submitting the poll form, how would you render the database form correctly, assuming the database form's rendering depends on some state information?

I was thinking the poll form would be submitted via ajax so the data record form would not have to be rendered, or on the submit of the "poll" the user is prompted that there changes will be lost and upon confirmation they are taken to the "poll results" page.

You are correct in thinking that if I wanted to return to that very page then every form on the page would require the same state information and every form would need the state widget defined with the same name.

But either way (the nice thing is) it is still a choice …

if the state is just for a single form, then explicitly using a hidden field rather than using getState/setState perhaps is easier and more intuitive.

Some of the logic to decide on how to serialize or how to store the state in cache and the cacheid is stored in the hidden field would be nice to have in a single spot.

Maybe a better widget would be a StatefulForm widget ? Which would combine the rendering of the form and the hidden field. The basic operation of this form would be then like prado's viewstate - all StatefulForms' use a common map to persist their state information and the getState / setState would be used to access this map. You can avoid rendering the "state" information in the form by using the CHtml::form syntax.

I'm still not quite clear yet on how to do this. Maybe you can think more about it and come up with some prototype so that we can continue to discuss?

Prototype 0.00001 ;D

The TStateFormatter is pretty much a copy from prado without the encryption or validation handling. It should also decide whether the state data should be cached or not.



<?php


class BStatefulForm extends CWidget {


  private static $_dataMap = false;


  const NAME='__yiiViewState';


  


  /**


   * Returns a statemap from the post or creates a new state map


   */


  public static function getStateMap() {


    if (self::$_dataMap===false) {


      // check to see if state was sent via request


      if (isset($_POST[BStatefulForm::NAME])) {


        $data = TStateFormatter::unserialize($_POST[BStatefulForm::NAME]);


        


        if ($data===false) {


          self::$_dataMap = new CMap;


        }


        else {


          self::$_dataMap = $data;


        }


      }


      else {


        self::$_dataMap = new CMap;


      }


    }


    


    return self::$_dataMap;


  }


  


  /**


   * Called by beginWidget


   */


  public function init() {


    echo CHtml::form($this->action='', 'post', $this->htmlOptions);


    if (self::$_dataMap!==false)


      echo CHtml::hiddenField(BStatefulForm::NAME, TStateFormatter::serialize(self::$_dataMap));


  }


  


  /**


   * Called by endWidget


   */


  public function run() {


    echo "</form>";


  }


  private $action=false;


  private $htmlOptions=array();


  public function setAction($action) {


    $this->action=$action;


  }


  public function setHtmlOptions($htmlOptions) {


    $this->htmlOptions=$htmlOptions;


  }


}


/**


    This needs to be enhanced to provide more security


 */


class TStateFormatter


{


	/**


	 * @param mixed state data


	 * @return string serialized data


	 */


	public static function serialize($data)


	{


    // TODO encrypt, use cache if availble 


    $str = serialize($data);


		if(extension_loaded('zlib'))


			$str=gzcompress($str);


		return base64_encode($str);


	}





	/**


	 * @param TPage


	 * @param string serialized data


	 * @return mixed unserialized state data, null if data is corrupted


	 */


	public static function unserialize($data)


	{


		$str=base64_decode($data);


		if(extension_loaded('zlib'))


			$str=@gzuncompress($str);


		if($str!==false)


		{


        $data = unserialize($str);


        return $data;


		}


		return false;


	}


}





?>


Usage controller



    $stateMap =BStatefulForm::getStateMap();


      


    if (!isset($stateMap['nextNumber'])) {


      $stateMap['nextNumber']=0;


    }


    else {


      $stateMap['nextNumber']+=1;


    }





usage page



<com:BStatefulForm action={array()}>


  <%= BStatefulForm::getStateMap()->itemAt('nextNumber') %>


  <%= CHtml::submitButton("Next") %>


</com:BStatefulForm>





The result is an incrementing number, if multiple forms exist on the same page then all forms numbers' are incremented.

The encryption & validation handling is why I thought this code should be part of a controller or maybe the application

nz

This looks interesting!

So basically if we want to keep state, the corresponding form has to be generated using this form widget, right? Looks good to me. Could you come up with more practical use cases?

That is the idea.

A practical case would be like a form wizard were you have multiple steps to complete the form, but all the information ends up in a single CActiveRecord. So you need to persist the data from post to post. Another role it could fill is to check for concurrent record modification.

nz

The implementation of this feature seems clear to me, but I'm not quite convinced by the use cases. In the use cases you gave, the persistent data is better kept in session rather than using hidden fields. So could you give more use cases?

The worst thing about storing information in a session is when a user opens a new window, now you have 2 windows open lets say the user navigates to one item and edits it on one window and then switches to the second window and edits a different record then they switch back and click save on the first window…

Or security - lets say a person is editing there own user profile and the unique identifier field value is included on a hidden field on the form - a smart hacker could simply change the value of this field to whatever and hit save overwriting maybe an admin account or something

nz

Session data can easily become corrupted  when used to persist state information. Having a piece of information that is secure on the form being submitted is the only way to ensure that a proper response is made.

Another example (of state usage) is preventing duplicate posts - this could be done using session information but it is also easily confused if the user has multiple browser windows open

nz

If you make a contact grabber from yahoo, hotmail or other service, when you get the list, you should show it to the user before the user accept / refuse each user.

It will be nice to have the requested ReST to compare the next page that everything is fine and not to request the same ReST again.

I think this is something important in Web 2.0.

What does this have to do with state widget?

They should be big, so you should save the data in state widget and not in the session.

Saving  that make your session state growth a lot. It's just an use case…

Sorry my bad english

Sebas

That is a good point, session space can be limited !

Just checked in the implementation of this feature.

Use CHtml::statefulForm() to render a form supporting persistent page state. And use CController::getPageState()/setPageState() to access persistent page state.

Let me know if you encounter any problem.

Excellent !

thanks qiang

++