Acerca de...
El equipo
Encuestas
WinTablets

Encuesta

Si las WINTABLETs no pudiesen ejecutar aplicaciones de escritorio, ¿las seguirías usando?

Cargando ... Cargando ...

últimas entradas importantes

Categorías

Archivos

09
Ago 2014
Desarrollorfogdevtrucos

Desarrollo: Control de edición con soporte para claves de acceso y hint

A veces te piden unas cosas que son de cajón pero tu plataforma parece que haya sido pensada con el extremo opuesto a la cabeza, y te toca hacer alguna viguería con lo que tienes disponible. Vamos aquí a explicar cómo tener un control con la funcionalidad descrita. Vamos a crear un control personalizado por herencia.

Si no sabes qué diferencia hay entre heredar y agregar respecto a los controles de Windows, ya os hice una pequeña introducción en una entrada anterior.

Puede ocurrirte que un control existente de Windows no cumpla todas tus expectativas. En mi caso la situación se produjo cuando necesitaba un control de edición que tuviera de forma simultánea un hint (eso que aparece como fondo cuando el campo está vacío y que se borra nada más meter texto) y la introducción de una contraseña (lo que tecleas se va convirtiendo en asteriscos).

Windows Phone (y Windows) tiene un control llamado PasswordBox que te presenta un cuadro de edición para introducir una clave de acceso y que sigue todas las reglas para ello. Pero no tiene hint.

Windows Phone tiene otro control llamado PhoneTextBox que es un campo de edición extendido que soporta, entre otras muchísimas cosas, el uso de un hint. Pero no tiene la opción de poner asteriscos (u otro carácter) como sustitutivo de lo tecleado. Si os fijáis en el artículo que os he enlazado más arriba, pese a que se llena la boca con la palabra password, en ningún lado pone lo de los asteriscos.

Lo cierto es que la situación termina resultando un poco kafkiana (me gusta el palabro, ¿qué pasa?). Si estuviéramos desarrollando con C++ quizás podríamos heredar una nueva clase a partir de las dos existentes, pero en .NET eso es un animal mitológico.

La solución pasa por crearnos un control que sea la mezcla de los dos. O bien podemos añadir al PasswordBox el tema del hint o bien añadir al PhoneTextBox el de los asteriscos. Si buscáis por internet hay varias soluciones implementadas, pero desde mi punto de vista todas ellas son demasiado alambicadas y complejas, sobre todo las que utilizan un control de usuario agregado.

Nosotros vamos a realizar una solución de compromiso y vamos a heredar directamente de PhoneTextBox para crear un nuevo control llamado PhonePasswordBox. Este nuevo control sólo tendrá una propiedad extra y un evento. Más conciso no se puede ser.

Primero os pongo el código y luego os lo explico:

    public partial class PhonePasswordBox:PhoneTextBox
    {
        private string m_password;

        public string Password
        {
            get { return m_password; }
        }

        public PhonePasswordBox():base()
        {
            m_password = String.Empty;
            this.TextChanged += PhonePasswordBox_TextChanged;
        }

        void PhonePasswordBox_TextChanged(object sender, system.Windows.Controls.TextChangedEventArgs e)
        {
            this.TextChanged -= PhonePasswordBox_TextChanged;
            var pl = m_password.Length;
            var tl = Text.Length;
            if (pl > tl)
                m_password = m_password.Substring(0, tl);
            else if (pl < tl)            
            {
                var car=Text[tl-1];
                m_password += car;                 
                if (tl > 1)
                {
                    Text = Text.Substring(0, tl - 2);
                    Text += "*";
                    Text += car;
                }
                Select(tl, 0);
            }
            this.TextChanged += PhonePasswordBox_TextChanged;
        }
   }

¿Cómo lo veis? Yo suelo poner el constructor justo debajo de la declaración de la clase, pero aquí he cambiado el orden por mor de sencillez.

Primero creamos una cadena miembro que va a almacenar lo que el usuario ha teclado, en conformidad con el control PasswordBox. Su propiedad, de sólo lectura, nos devuelve el valor.

Luego tenemos el constructor, que previamente a su construcción llama al de la clase padre para luego asignar un valor no nulo a lo que el usuario va a teclear y lo más importante, añadimos un nuevo método miembro al evento TextChanged de nuestra clase, que como hemos heredado de PhoneTextBox, se corresponderá a su propio evento, que se dispara cada vez que el texto editado cambia.

Y luego tenemos el evento en sí, el que nos va a cambiar la letra por el asterisco. Y no, no es tan fiero como parece. Básicamente lo que hace es medir el tamaño de la cadena que se está mostrando y el guardado. Si el tecleado es menor, se que se ha borrado un carácter, lo que hacemos en el de la clave. Si es mayor, es que se ha añadido uno nuevo, y tenemos que cambiar el anterior por un asterisco y poner el nuevo en el campo de edición, y añadirlo a nuestra cadena de clave.

La línea del Select() nos pone el cursor al final del cambio de edición, pero solo cuando se haya añadido algo.

¿Qué falta? La desconexión y conexión del evento cuando entramos y salimos de él. ¿Alguien se imagina el motivo? … Efectivamente, en cuanto toquemos la propiedad Text del control, se volverá a disparar de forma recursiva. Con el código que os muestro no pasa nada, pero en otras situaciones sí y haría que nuestra aplicación generase una excepción por desbordamiento de pila.

El único problema aquí sería que el evento se llamaría recursivamente varias docenas de veces ante cada pulsación del usuario, lo que si bien no afecta a nada, sí que vuelve la edición un poco más lenta.

Una vez que tenemos el código escrito, usar el control es tan sencillo como con cualquier otro. Sólo tenemos que añadir en la página XAML el espacio renombres y luego usarlo en referencia a él:

 

 xmlns:customControls=“clr-namespace:MiPrograma.UI.CustomControls”

<customControls:PhonePasswordBoxName=“PasswordTextBox”MaxLength=“30”Hint=“{Binding Path=LocalizedResources.PasswordHint, Source={StaticResource LocalizedStrings}}”InputScope=“Password”/>

 

¿Os mola?

A mi no, porque es un compromiso entre funcionalidad y diseño. Respecto a lo que sería un control en condiciones es una chapuza. Por ejemplo, el método Text sigue estando disponible para el usuario, con lo que podría usarlo para leer una cadena de asteriscos en lugar del valor real tecleado.

Podríamos ocultar la propiedad conforme a lo explicado antes, pero entonces se producen efectos laterales no deseados porque lo más seguro es que el propio control padre, PhoneTextBox, tampoco tenga su código limpio en exceso. Si queréis podéis hacer la prueba. Usando el ejemplo de la semana pasada ocultad el campo y haced llamadas a “base.Text” en el evento en lugar de “Text”. Que san Apapurcio os pille confesados. :-P

Otra chapuza es quitar y poner el evento. La solución a esto es redefinir un nuevo evento con el mismo nombre y ocultar el antiguo, y luego redireccionar el que se haya podido suscribir el usuario al antiguo y nosotros usar el nuevo, poniendo un bloqueo para evitar que se dispare el del usuario cuando nosotros estemos trasteando con el campo de texto, lo que nos lleva al punto anterior sobre la ocultación de dicho campo…

Pero como no vamos a vender el control, lo que hemos hecho nos basta.

Por RFOG | 13 Comentarios | Enlaza esta entrada

13 Comentarios

Juan Quijano
Enviado el 09/08/2014 a las 09:11 | Permalink

Con todo el cariño del mundo, pero este código huele fatal.

Variables no autoexplicativas? Usando else if?
Quitando suscripciones de forma dinámica?

Uffff, seguro que se puede hacer de forma mucho mas limpia…

    RFOG
    Enviado el 09/08/2014 a las 10:47 | Permalink

    Con todo el cariño del mundo, no te has leído la entrada y sólo has mirado el código.

    1.- Esas mismas objeciones que tu pones en el comentario las pongo yo en la entrada, excepto la de variables no autoexplicativas. De todos modos pl es (P)assword (L)ength y la otra la dejo a tu imaginación.

    2.- Si miras en toda mi trayectoria publicando ejemplos de código, todos van sin comentarios, porque lo que quiero que se vea es el código, no los comentarios, y si para es bloque de código que he puesto los necesitas… bueno, mejor te dedicas a otra cosa. De todos modos el texto de abajo explica cómo va la cosa. De nuevo la única pega a ese código es el de las dos variables pl y tl.

    3.- ¿Cuál es el problema del else if? Si es menor o si es mayor, está claro como el agua.

    4.- Te reto a que quites de forma dinámica la suscripción. (Te adelanto que la gente de JetBrains no sabe cómo hacerlo). Mira, ya tienes para una entrada aquí.

    5.- Te reto a que mejores ese código. Escribe una entrada nueva mejorándolo.

    6.- Escribe algo en WinTablet, lo que sea.

    Enviado el 09/08/2014 a las 11:48 | Permalink

    Bueno, siempre hay ocasiones en las que hay que hacer las cosas “quick and dirty”. Yo soy un virtuoso y a veces consigo hacerlas incluso “slow and dirty”. :mrgreen:

Enviado el 10/08/2014 a las 11:24 | Permalink

Pues a ver, sin ningún cariño :mrgreen: pero con ánimo constructivo, salvo que se me haya escapado algo del código (lo cual con mi despiste no sería raro) creo que el apaño no debe funcionar bien. He hecho un seguimiento de los valores entrantes y salientes cada vez que se añade un carácter (suponiendo que se teclea “abcd”) y la cosa no cuadra:

Text (a la entrada) m_password (a la entrada) pl tl m_password (a la salida) Text (a la salida)
a 0 1 a a
ab a 1 2 ab *
*c ab 2 2 ab *c
*cd ab 2 3 abd **

Además, al margen de que se me haya podido escapar algo y sí que cuadre, lo que seguro que no cuadrará es si elimino algún carácter que no sea el último, o sea muevo el cursor a la mitad del password y entonces elimino.

    RFOG
    Enviado el 10/08/2014 a las 13:50 | Permalink

    El código ese está en producción varias semanas y funciona perfecto, así que revisa. Bueno, el martes miro a ver si es una versión anterior. Pero tienes razón con lo del cursor en medio. Lo verifico el martes.

      Enviado el 10/08/2014 a las 14:55 | Permalink

      A ver, el único sitio del código donde veo que se añaden los asteriscos es en el dentro del if, la primera vez que se pasa (con solo 1 carácter tecleado) tl no es mayor que 1 (es justo 1), con lo cual no se agrega el asterisco. ¿Me pierdo algo?

      Enviado el 10/08/2014 a las 15:21 | Permalink

      OK, acabo de ver que me había saltado a la torera una línea de código. Ahora (quitando lo de borrar un carácter que no sea el último) sí funciona:

      Text (a la entrada) m_password (a la entrada) pl tl m_password (a la salida) Text (a la salida)
      a 0 1 a a
      ab a 1 2 ab *b
      *bc ab 2 3 abc **c
      **cd abc 3 4 abcd ***d

      Aún así, veo que el último carácter queda siempre visible. Estaría bien añadir un timer que cambie también el último caracter pasados x milisegundos desde que se introdujo.

        RFOG
        Enviado el 11/08/2014 a las 11:06 | Permalink

        El último caracter visible está puesto a propósito, y timers ya tiene unos cuantos la aplicación como para poner uno más.

        Respecto a lo de editar sin estar en el extremo, si existe el método para saber dónde está el cursor dentro de la cadena, es tan fácil como añadir quitar el carácter a partir de esa posición. Mañana me pongo con ello en el curro.

          Enviado el 11/08/2014 a las 12:57 | Permalink

          Hombre, por el código ya veo que está hecho a propósito. Yo veo bien que se vea por unos momentos, como por ejemplo en Android, así sirve como feedback de que has pulsado la tecla correcta, pero me parece fatal que ese último carácter se quede visible indefinidamente hasta que se introduzca otro carácter. Supongo que es cuestión de gustos.

          RFOG
          Enviado el 11/08/2014 a las 13:59 | Permalink

          Creo que iOS va a sí, deja el último carácter.

Enviado el 11/08/2014 a las 13:44 | Permalink

Respecto a lo de quitar y volver a quitar y volver a añadir el evento, no sé si lo que voy a decir es más eficiente, menos eficiente o igual, pero lo que yo hago a veces en estos casos es crear una variable en el objeto, llamada por ejemplo bypass con valor false. Si al entrar en el evento la variable bypass es true salgo inmediatamente. Si en cambio es false, la pongo a true, trasteo lo necesario, y la vuelvo a poner a false antes de salir. De esa forma cuando al trastear se dispare el evento no hará nada sino que volverá inmediatamente porque bypass es true.

    RFOG
    Enviado el 11/08/2014 a las 14:05 | Permalink

    Lo probé y no funcionó, porque la llamada es recursiva y reentrante, y tampoco sabes qué hace el sistema con ese evento.

    Esa es una gran pega de C# y el tema de los eventos. El quitar el evento así a lo bruto es una chapuza, pero es la única forma documentada que hay de hacerlo, de hecho el propio sistema es lo que hace. En C# no hay forma de mirar si el evento está o no, o si está en ejecución y la puedes liar parda.

    En C++/CLI hay una forma bastante mierdosa de hacerlo, pero tienes que tener en cuenta si el evento es multicast o no, y más cosas.

    Con lo dicho, el reto está en alto: un gallifante para quien consiga dessuscribir un evento de forma segura y sin trucos. De momento, la gente de JetBrains, que son unos cracks de C#, no saben cómo hacerlo. Ni creo que la propia Miscrosoft. Es una de tantas aberraciones que tiene C# y el .NET en sus tripas.

Deja un comentario  

Tu email nunca se publica o se comparte. Los campos obligatorios están marcados con *

*
*
:wink: :-| :-x :twisted: :) 8-O :( :roll: :-P :oops: :-o :mrgreen: :lol: :idea: :-D :evil: :cry: 8) :arrow: :-? :?: :!:
Puedes usar las siguientes etiquetas y atributos HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

contacto@wintablet.info tema WinTablet.info por Ángel García (Hal9000)