Foros - Stratos

Programadores => General Programadores => Mensaje iniciado por: XÑA en 11 de Febrero de 2011, 12:37:34 PM

Título: ¿Qué os parece este método para hacer un Undo?
Publicado por: XÑA en 11 de Febrero de 2011, 12:37:34 PM
Código (csharp) [Seleccionar]

public static class UndoRedo
{
static List<Action> acciones = new List<Action>();
static bool undoing = false;

public static void Add(Action accion)
{
if(!undoing)
acciones.Add(accion);
}

public static void Undo()
{
Action accion = acciones[acciones.Count - 1];

undoing = true;

accion();

undoing = false;

acciones.RemoveAt(acciones.Count - 1);
}

}

class Prueba
{
public int X, Y;
public List<int> Lista;

public Prueba(int a, int b)
{
X = a;
Y = b;

Accion();
}

public void SetX(int v)
{
UndoRedo.Add(() => { SetX(X); });

X = v;
}
public void Accion()
{
Lista = new List<int>();

Lista.Add(X);
Lista.Add(Y);

}
}

class Test
{
public List<Prueba> lista = new List<Prueba>();

public void Add(Prueba v)
{
lista.Add(v);

UndoRedo.Add(() => { RemoveLast(); });
}

public void Cambia(int i, Prueba v)
{
Prueba old = lista[i];

lista[i] = v;

UndoRedo.Add(() => { Cambia(i, old); });
}

public void RemoveLast()
{
int i = lista.Count - 1;

Prueba old = lista[i];

UndoRedo.Add(() => { Add(old); });

lista.RemoveAt(i);
}
}

public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{

Test t = new Test();

t.Add(new Prueba(1, 2));
t.Add(new Prueba(3, 4));

// Nos elimina el último Add
UndoRedo.Undo();

// Modificamos un valor
t.lista[0].SetX(99);

// Eliminamos el último, que en este caso es Prueba(99,2)
t.RemoveLast();

// Recuperamos Prueba(99,2)
UndoRedo.Undo();
}
}


Comments, please... :D
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: blau en 11 de Febrero de 2011, 01:40:24 PM
es el que yo uso ;)

es acojonante lo fácil que es con delegados anónimos.

Creo que ya escribi sobre esto.... si, aquí http://www.stratos-ad.com/forums/index.php?topic=13509.msg143073#msg143073 ;)

Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: XÑA en 11 de Febrero de 2011, 02:11:39 PM
Pues sí, está muy bien, pero no entiendo muy bien pq le pasas un método para el Redo.

Como puedes ver, en la clase Test.RemoveLast() eso se controla automáticamente.

¿Hay alguna razón pq lo hayas hecho así?

Otra cosa: este sistema NO funciona. Yo creía que me guardaba un delegado con los valores adecuados cada vez que se llamaba al método Add, pero no es así, lo he descubierto con esto:


         t.lista[0].SetX(99);
            t.lista[0].SetX(90);
            UndoRedo.Undo();

Al entrar en el delegado para hacer el Undo
      public void SetX(int v)
      {
         UndoRedo.Add(() => { SetX(X); });

         X = v;
      }


lo que hace es llamar a SetX, PERO CON EL VALOR ACTUAL DE X, no con el valor que tenía cuando he añadido la acción...  >:(
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: blau en 11 de Febrero de 2011, 07:29:01 PM
tienes que crear una variable en la pila, y asignar el valor para que te funcione.

algo asi:

  public void SetX(int v)
      {
        int Backup = X;
         UndoRedo.Add(() => { SetX(Backup); });

         X = v;
      }

De esta forma no te guarda una referencia a X sino a backup, y backup esta en la pila, como  lo hace no me lo preguntes, pero funciona.


Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: XÑA en 11 de Febrero de 2011, 07:35:48 PM
No eso no funciona, ya lo he probado. Lo que te hace es que en el método anónimo te añade ese código como si lo escribieras dentro del método anónimo.  >.<

Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: blau en 11 de Febrero de 2011, 09:23:34 PM


Date cuenta que la variable backup se declara fuera del método anónimo y que es de tipo struct, no una referencia.

Eso lo tengo mas que requeteprobado... y funciona. ;)
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: XÑA en 11 de Febrero de 2011, 09:37:49 PM
Pues a mi no me funciona, me coge el valor actual. Y es lógico, porque el método anónimo es un delegado, que es un método al que llama la función. Si hace lo que tu dices, eso significa que CADA vez que estoy haciendo el lambda, me está creando 1 delegado nuevo, y eso no me parece muy lógico.

Pero ya que dices que te funciona, lo probaré más conciencudamente.  :D

¡Gracias!  :)
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 14 de Febrero de 2011, 11:19:40 AM
Cuando vuelva a casa respondo a esto :P
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 14 de Febrero de 2011, 03:04:10 PM
Contesto al tema de las variables y de paso al thread the blau que se quedó sin respuesta en su día: los delegados inline y las lambda en C# funcionan como "clausuras". Como la wikipedia lo define muy bien lo copio :p

"En Informática, una clausura es una función que es evaluada en un entorno conteniendo una o más variables dependientes de otro entorno. Cuando es llamada, la función puede acceder a estas variables."

En C#, las clausuras capturan las variables, no sus valores, por eso le pasa a XÑA que captura X, pero como es un atributo y se modifica, luego cuando la lambda accede a X se encuentra otro valor que el esperado. En Java por ejemplo se capturan los valores y no las variables.

Y por eso el truco de usar variables locales, la variable local se captura al igual que el atributo, pero te aseguras que nadie la va a cambiar (ya que no se ve fuera del método, y si vuelves a llamar a ese método se crea una variable local nueva diferente).

Al que quiera leer algo más en detalle:

http://csharpindepth.com/Articles/Chapter5/Closures.aspx

El compilador al final implementa todo esto como una clase, por ejemplo por eso no se pueden hacer estas cosas:


static void Main(string[] args)
{
    int a = 0;
    Action<int> myAction = (a => a += 5);
}


El que quiera más detalle en la especificación de C# (http://msdn.microsoft.com/en-us/vcsharp/aa336809) que mire el apartado 7.14.1. Un saludo!

Vicente
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 14 de Febrero de 2011, 03:14:31 PM
Y sobre la clase en sí de XÑA un comentario: porque no usas una pila en vez de una lista?

Y luego otra cosa: el flag ese de undoing no funciona (o eso creo si entiendo tu intención bien). Primero, porque lo que deberías evitar que se añada algo justo cuando se va a sacar algo (para realmente deshacer lo que quieres deshacer), no tiene mucho sentido (parece) evitar añadir algo mientras otra cosa se deshace. Parece que usas el flag para tener el índice correcto en el RemoveAt del final, pero usar una pila ya te salvaría de este problema.

Pero de todas formas, intentar sincronizar hilos con ese flag va a petar fijo antes o después. Tendrías que usar las clases específicas para esto (Monitor y demás), pero si quieres, ya tienes el trabajo hecho en .NET 4.0 con la clase ConcurrentStack<T>:

http://msdn.microsoft.com/en-us/library/dd267331.aspx

Un saludo!
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: XÑA en 15 de Febrero de 2011, 12:35:49 PM
Pero Vicente, para hacer eso que tu dices de la variable, c# necesitaría crear una clase CADA VEZ que llamo al método. Así que tendría miles de clases...
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 15 de Febrero de 2011, 12:55:21 PM
Y?
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: XÑA en 15 de Febrero de 2011, 01:48:16 PM
Pues imagínate la de miles de clases que se crean...Pero en fin...

Bueno, lo he probado y funciona, se ve que hice algo mal.  :-\

De todas formas, he modificado la implementación y estoy probando pasar parámetros. Pronto os pasaré el código... ^_^
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 15 de Febrero de 2011, 01:54:46 PM
El compilador hace un montón de cosas por detrás que no te enteras normalmente (también se generan clases cuando haces yield por ejemplo, y muchos métodos de Linq están hechos así), además este detalle es algo específico de la implementación no de la especificación, lo mismo Mono hace otra cosa (ni idea).

De todas formas al framework crear miles de clases le da igual, no es un problema en absoluto (si estás con el CF ya es otra cosa :p).
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: XÑA en 15 de Febrero de 2011, 02:49:03 PM
A ver así:


            UndoRedo.Registro r = new UndoRedo.Registro();
            r.Name = "SetX=" + X.ToString();
            r.MetodoParams = (a) => { SetX((int)a[0]); };
            r.Parametros = new object[] { X };
            UndoRedo.Add(r);

¿Me creará una clase sólamente o cada vez que se ejecute el código creará una nueva?

Luego estoy con un problema. Resulta que yo no mantengo lista de Redo, es lo mismo que el Undo, pero al revés. Por eso tengo un índice y no una pila, aunque bueno, podría usar dos pilas... Pero en fin, de momento estoy con esto.

Lo chuungo de tener sólo la lista de Undo es lo siguiente. Si por ejemplo yo hago esto:

SetX(1);  -> En UndoRedo, se añade el valor que había antes, por ejemplo un 0.
SetX(2);  -> En UndoRedo, se añade el valor que había antes (1)
SetX(3);  -> En UndoRedo, se añade el valor que había antes (2)

Hago Undo, y X pasa a valor 2.
Hago Undo, y X pasa a valor 1.
Hago Redo, y X pasa a valor 2.

¡Pero el problema es que no tengo la información de que sea 3 de nuevo!
Claro, añadir al método Add el valor del redo me parece muy fuerte, porqué sólo falla en el último valor de la lista... :'(

¿alguna idea?  8)

Gracias...
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: blau en 15 de Febrero de 2011, 03:13:10 PM
Cita de: Vicente en 14 de Febrero de 2011, 03:04:10 PM
En C#, las clausuras capturan las variables, no sus valores, por eso le pasa a XÑA que captura X, pero como es un atributo y se modifica, luego cuando la lambda accede a X se encuentra otro valor que el esperado. En Java por ejemplo se capturan los valores y no las variables.

Y por eso el truco de usar variables locales, la variable local se captura al igual que el atributo, pero te aseguras que nadie la va a cambiar (ya que no se ve fuera del método, y si vuelves a llamar a ese método se crea una variable local nueva diferente).

Me alegro de saber en detalle a que se debe, pero dejas mi pregunta todavia sin responder.  Y es si hay alguna forma de decirle al compilador que la variable que le pasas al metodo lambda debe cogerla por valor, o algo asi como una copia.


@xña: si, cada vez que generas una accion estas definiendo un nuevo lambda... pero y ¿lo que te ahorras en programacion? si esto hubiese que hacerlo a pelo habria que liar tela... y asi es super elegante, en la clausura se guardan todas las variables afectadas de una forma transparente y supercomoda.
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: blau en 15 de Febrero de 2011, 03:24:36 PM
Uff, estoy muy espeso y no pillo el ultimo codigo.

Te pongo el mio entero:

Clase HelperUndo.cs

public class HelperUnDo : HelperComponentScene
   {
       
       public static HelperUnDo Instancia { get; private set; }

       class Data
       {
           public string Operation;
           public Action Undo;
           public Action Redo;

           public override string ToString()
           {
               return Operation;
           }
       }

       FormUndo form = new FormUndo();

       HelperUnDo()
       {
           Instancia = this;
           InputManager.Attach(Game);
           //form.Show();
       }

       Stack<Data> UndoActions = new Stack<Data>();
       Stack<Data> RedoActions = new Stack<Data>();

       public static void AddUnDo(string operation, Action undo, Action redo)
       {
           Data data = new Data() { Operation=operation, Redo = redo, Undo = undo };
           Instancia.UndoActions.Push(data);
           Instancia.OnUndoPush(data);
       }
       
       public override void Update(Microsoft.Xna.Framework.GameTime gameTime)
       {
           if (UndoActions.Count>0 && InputManager.Instancia.Keyboard.KeyHit(Microsoft.Xna.Framework.Input.Keys.Z) && InputManager.Instancia.Keyboard.KeyPressed(Microsoft.Xna.Framework.Input.Keys.LeftControl))
           {
               Data data = UndoActions.Pop();
               RedoActions.Push(data);
               data.Undo.Invoke();
               OnUndoPop(data);
           }
           else if (RedoActions.Count > 0 && InputManager.Instancia.Keyboard.KeyHit(Microsoft.Xna.Framework.Input.Keys.Y) && InputManager.Instancia.Keyboard.KeyPressed(Microsoft.Xna.Framework.Input.Keys.LeftControl))
           {
               Data data = RedoActions.Pop();
               UndoActions.Push(data);
               data.Redo.Invoke();
               OnUndoPush(data);
           }

           base.Update(gameTime);
       }
       
       private void OnUndoPop(Data data)
       {
           form.ListUndo.Items.Remove(data);
       }

       private void OnUndoPush(Data data)
       {
           form.ListUndo.Items.Add(data);
       }
   }

Ejempo de uso al final de un drag en el editor:

Vector2 BeginDraggingPosition = beginDragVector2;
Vector2 EndDraggingPosition = Transform.Position;                
HelperUnDo.AddUnDo("Transform Translation", delegate() { Transform.Position = BeginDraggingPosition; }, delegate() { Transform.Position = EndDraggingPosition; });                
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 15 de Febrero de 2011, 03:47:18 PM
Cita de: blau en 15 de Febrero de 2011, 03:13:10 PM
Me alegro de saber en detalle a que se debe, pero dejas mi pregunta todavia sin responder.  Y es si hay alguna forma de decirle al compilador que la variable que le pasas al metodo lambda debe cogerla por valor, o algo asi como una copia.

Nope.
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 15 de Febrero de 2011, 04:12:24 PM
blau por curiosidad porque has usado un delegado y no una lambda?
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 15 de Febrero de 2011, 04:15:22 PM
Y respecto a tu duda XÑA, poco que añadir al código de blau, está de pm :)
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: blau en 15 de Febrero de 2011, 04:22:35 PM
Cita de: Vicente en 15 de Febrero de 2011, 04:12:24 PM
blau por curiosidad porque has usado un delegado y no una lambda?

He de confesar de que cuando hice aquello los lambda y yo no nos llevavamos
,aunque ahora admito en este caso que solo es una sentencia lo hubiese hecho con un lambda. :)

por cierto,  a la hora de la verdad serian equivalentes, ¿no?

delegate () { a = b}   <=>  () => { a = b}
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: reduzio en 06 de Marzo de 2011, 04:19:39 AM
Hola! Aca les paso como funciona mi sistema de undo/redo, citado de cuando lo postie en el foro de ADVA, esta en C++ pero nada impide hacerlo en java, C# u otro lenguaje (de hecho, el mas dificil es C++). Es muy practico, se usa muy poco codigo y no requiere escribir clases para cada comportamiento (template magic), pero puede llevar unas leidas entenderlo.
Lo utilice en proyectos importantes y doy fe que es muy practico, pero probablemente hay que pensar hacer las APIs para que sean compatibles. Espero que sea de utilidad!

Citar


La alternativa que planteo se basa en usar dynamic typing en los lenguajes que lo soportan, o variants/template magic en C++.
La idea es que, en vez de crear una clase por cada operacion, hay que hacer que cada comando recuerde una lista de llamadas a funciones/metodos con sus respectivos parametros. Si el codigo esta bien encapsulado, esto deberia funcionar en la mayoria de los casos (o todos). Ejemplo:

undo_redo->begin_comand()
undo_redo->add_do_method( sprite, &Sprite::set_pos, 20, 20 );
undo_redo->add_undo_method( sprite, &Sprite::set_pos, 5, 5);
undo_redo->end_command();

Y listo! Esto es muy comodo porque sirve para modificar codigo existente para que pueda hacer undo/redo, sin tener que reencapsular todo el codigo en clases o comandos.

Algunos Problemas..

Un problema comun al hacer un sistema de undo/redo es que hacer cuando hay que poner en un stream, nuevas instancias (o borrar existentes).
Lo mas comodo es guardar comandos trabajando sobre punteros, asi que lo ideal es no guardar comandos que "crean" instancias, sino crear las instancias de forma externa y luego usarlas en comandos, ejemplo:

Se intenta crear un sprite y ponerlo en el sprite_manager:

sprite = new Sprite;

undo_redo->begin_comand()
undo_redo->add_do_method( sprite_manager, &SpriteManager::add_sprite, sprite );
undo_redo->add_undo_method( sprite_manager, &SpriteManager::remove_sprite, sprite );
undo_redo->add_do_reference(sprite);
undo_redo->end_comand();

En C o C++ el comando tiene que guardarse el puntero a sprite, ya que si en algun momento el usuario decide hacer undo, y luego proceder de otra forma, la referencia al sprite se va a perder (a menos que sea un autopointer o un lenguaje dinamico) y debera ser borrado (con delete).

De forma opuesta sucede cuando se retira un sprite y se borra:

undo_redo->begin_comand()
undo_redo->add_do_method( sprite_manager, &SpriteManager::remove_sprite, sprite );
undo_redo->add_undo_method( sprite_manager, &SpriteManager::add_sprite, sprite );
undo_redo->add_undo_reference(sprite);
undo_redo->end_comand();

En este caso "sprite" se guarda como undo reference. Esto sucede porque las listas de undo/redo son te tamanio fijo (ejemplo, hasta 50 operaciones) y cuando los comandos llegan al final de la lista, son eliminados. De esta forma, un objeto que fue removido, es realmente eliminado cuando el comando se elimina.
Título: Re: ¿Qué os parece este método para hacer un Undo?
Publicado por: Vicente en 06 de Marzo de 2011, 09:21:38 AM
Cita de: blau en 15 de Febrero de 2011, 04:22:35 PM
por cierto,  a la hora de la verdad serian equivalentes, ¿no?

delegate () { a = b}   <=>  () => { a = b}

Se me había pasado esta pregunta. La respuesta corta es "a veces".

La respuesta larga es un poco mas complicada. Primero hay que saber que una lambda puede representar tanto un delegado como un arbol de expresiones, por ejemplo:

Citar
Action act = () => Console.WriteLine(a);
Expression<Action> exp = () => Console.WriteLine(a);

En .NET el ejemplo mas claro de esto es LINQ to Objects, donde se utiliza IEnumerable<T> y los metodos de LINQ reciben delegados, frente a LINQ to SQL, donde se utiliza IQueriable<T> y los metodos de LINQ reciben arboles de expresiones (que luego se traducen a SQL).

Pero hay que tener en cuenta que no todas las lambdas pueden traducirse a arboles de expresiones, lo siguiente no compila:

Citar
Action act2 = () => a = 10;
Expression<Action> exp2 = () => a = 10; // Error

Los arboles de expresiones no pueden tener asignaciones (de momento).

En cambio los metodos anonimos no pueden transformarse en arboles de expresiones. Lo siguiente tampoco compila:

Citar
Func<int> func = () => 10;
Func<int> func2 = delegate() { return 10; };
Expression<Func<int>> exp3 = delegate() { return 10; }; // Error

Hay mas detalles que dejan ver que este tipo de cosas se manejan de manera un poco excepcional por el compilador, por ejemplo esto no compila porque directamente no esta permitido:

Citar
var var = () => 10;
var var2 = delegate() { return 10; };

Pero bueno, en general son casos un poco raros...