Stratos: Punto de Encuentro de Desarrolladores

¡Bienvenido a Stratos!

Acceder

Foros





¿Qué os parece este método para hacer un Undo?

Iniciado por XÑA, 11 de Febrero de 2011, 12:37:34 PM

« anterior - próximo »

XÑA

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

blau

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 ;)


XÑA

#2
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...  >:(

blau

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.



XÑA

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.  >.<


blau



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. ;)

XÑA

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!  :)


Vicente

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

Vicente

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!

XÑA

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...


XÑA

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... ^_^

Vicente

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).

XÑA

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...






Stratos es un servicio gratuito, cuyos costes se cubren en parte con la publicidad.
Por favor, desactiva el bloqueador de anuncios en esta web para ayudar a que siga adelante.
Muchísimas gracias.