Como detectar que se ha modificado los items de un ItemsControl

Después de unos días en PortAventura gritando como un loco en el Furius Baco (teneis que probarla) os cuento un problema que me surgió, tenia que detectar cuando había sido añadido o eliminado items de un ItemsControl, en este caso una ListBox, estos items podían ser añadidos y eliminados sin mi control solo cambiaba la colección. Supuse que seria bastante fácil, que estaría el  evento CollectionChanged pero cuando me puse a mirar me lleve la decepción que no estaba, no me creía que no estuviese así que me dedique a mirar mas a fondo y vi que estaba pero no estaba explícitamente implementado ya que INotifyCollectionChanged que es la que contiene este manejador no esta implementado explícitamente.

La manera de solucionarlo, con un casting

  1. public MyWindow()  
  2. {  
  3.     InitializeComponent();  
  4.       
  5.     ((INotifyCollectionChanged)mListBox.Items).CollectionChanged +=  
  6.         mListBox_CollectionChanged;  
  7. }  
  8.   
  9. private void mListBox_CollectionChanged(object sender,    
  10.     NotifyCollectionChangedEventArgs e)  
  11. {  
  12.     if (e.Action == NotifyCollectionChangedAction.Add)  
  13.     {  
  14.         // realizar el scrool hasta el nuevo item  
  15.         mListBox.ScrollIntoView(e.NewItems[0]);  
  16.     }      
  17. }    

No es la manera mas elegante pero me sirvió

Como interceptar las acciones de Copy/Paste

Esto surgió en los foros de MSDN y la verdad es que me pareció interesante ya que puede haber situaciones en las que no queramos que el usuario copie datos para pegarlos en otra aplicación e incluso obligarle a que introduzca los datos por teclado en vez de que haga un paste.

Me puse a investigar y en la clase System.Windows.DataObject me encontré una serie de métodos estáticos que resolvían este problema

 

DataObject.AddPastingHandler(dependencyObject, handler);  

DataObject.RemovePastingHandler(dependencyObject, handler);  

 

DataObject.AddCopyingHandler(dependencyObject, handler);  

DataObject.RemovePastingHandler(dependencyObject, handler);  

 

DataObject.AddSettingDataHandler(dependencyObject, handler);  

DataObject.RemoveSettingDataHandler(dependencyObject, handler); 

 

Por ejemplo

 

  1. <TextBlock Text=»Regular TextBox: « Grid.Row=»0″ Grid.Column=»0″ />
  2.             <TextBox x:Name=»_TextBlock1″ Grid.Row=»0″ Grid.Column=»1″ />
  3.             <TextBlock Text=»No Copy: « Grid.Row=»1″ Grid.Column=»0″ />
  4.             <TextBox x:Name=»_TextBlock2″ Grid.Row=»1″ Grid.Column=»1″ />
  5.             <TextBlock Text=»No Drag Copy: « Grid.Row=»2″ Grid.Column=»0″ />
  6.             <TextBox x:Name=»_TextBlock3″ Grid.Row=»2″ Grid.Column=»1″ />
  7.             <TextBlock Text=»No Paste: « Grid.Row=»3″ Grid.Column=»0″ />
  8.             <TextBox x:Name=»_TextBlock4″ Grid.Row=»3″ Grid.Column=»1″ />
  1.   public Window1()
  2.     {
  3.       InitializeComponent();
  4.       DataObject.AddCopyingHandler(_TextBlock2, NoCopy);
  5.       DataObject.AddCopyingHandler(_TextBlock3, NoDragCopy);
  6.       DataObject.AddPastingHandler(_TextBlock4, NoPaste);
  7.     }
  8.     private void NoCopy(object sender, DataObjectCopyingEventArgs e)
  9.     {
  10.       e.CancelCommand();
  11.     }
  12.     private void NoDragCopy(object sender, DataObjectCopyingEventArgs e)
  13.     {
  14.       if (e.IsDragDrop)
  15.       { e.CancelCommand(); }
  16.     }
  17.     private void NoPaste(object sender, DataObjectPastingEventArgs e)
  18.     {
  19.       e.CancelCommand();
  20.     }
  21.   

Como veis también podemos impedir el Drag de nuestros elementos a otras aplicaciones, una manera de asegurarnos que nuestros datos no saldrán de nuestra aplicación.

Como saber lo que quiere decir XamlParseException

Hay veces que te vuelves loco cuando te da la excepción XamlParseException, te aparece tan ricamente sin darte ninguna aproximación de donde se ha producido el error, esto lo podemos solucionar sacando el log del error a la ventana de output para ver cual ha sido realmente el problema, por ejemplo

 

image

image 

Para activar esta característica solo tenemos que meter 3 líneas de código en nuestra aplicación

 

  1. public Window1()
  2.         {
  3.             PresentationTraceSources.Refresh();
  4.             PresentationTraceSources.MarkupSource.Switch.Level = SourceLevels.All;
  5.             PresentationTraceSources.MarkupSource.Listeners.Add(new DefaultTraceListener());  
  6.             ……

Cuando se cree la ventana y exista un error en el XAMNL nos lo mostrará en la ventana de output

Snoop una utilidad para hacer debug visual en WPF

Una de las cosas que deseaba en WPF cuando empecé  aplicaciones complejas es poder realizar debug visuales y con esto me refiero a que cuando ejecuto la aplicación y la interfaz tiene muchos elementos a veces no aparecen como uno cree que ha diseñado y desearía una herramienta en la que pudiéramos “debugear” el árbol visual y aquí encontré Snoop una herramienta muy valiosa en mis desarrollos.

Podéis bajarla desde aquí, si la ejecutáis veréis

image

lo que tiene es un combo en el que nos muestran todas las aplicaciones hechas en WPF que están corriendo en nuestro sistema y como podéis ver en la imagen Blend esta hecho en WPF. Una vez seleccionado, pulsáis en los prismáticos para indicarle que es la aplicación que queremos observar, en ese momento aparece una nueva pantalla en la que nos muestra el árbol visual de la aplicación

image

en cada elemento del árbol visual podremos ver sus propiedades y los eventos enrutados, podremos buscar nuestro elemento entre todo el árbol visual y una funcionalidad que a mi me gusta mucho es que cuando selecciono un elemento en la aplicación WPF esta aparece con un Border rojo para que lo veamos visualmente. por ejemplo si selecciono de Blend este elemento

 

image

En la aplicación Blend podemos ver a cual corresponde

image

De esta manera podemos ver lo que ocupa, donde esta colocado, si hay algún otro elemento encima…, también nos ofrece verlo si pasamos el ratón por encima del elemento, snoop nos mostrará una vista de solo ese elemento, como podéis ver en la imagen

 

image

Otra funcionalidad importante, es que nos va a permitir ver que elementos tienen errores de Binding, para ello en el combo de la pate izquierda elegimos la opción “Visuals with Binding errors” y nos mostrará los elementos con errores de Binding en el Blend tenemos

image

 

 

 

Nos muestra en rojo la propiedad con el Binding erróneo así como el error.

image

Espero que os haya gustado tanto como a mi

SplashScreen en WPF

Con la llegada del SP1 del FrameWork 3.5 nos facilito la posibilidad de hacer SplashScreenen nuestras aplicaciones. Esto nos permite crear una imagen que será mostrada cuando arranque la aplicación mientras la aplicación se carga.

Es muy sencillo de realizar, añadimos la imagen que queremos a nuestro proyecto y en la propiedad BuildAction elegimos SplashScreen

 

     image                                               image

Y ya esta, nos mostrará esta imagen mientras se carga la aplicación, pero a veces nos interesa controlar el tiempo en que se muestra la imagen , para ello debemos de cargar manualmente la pantalla de Splash, esto lo realizaremos en el evento Application_Startup y cambiando la propiedad BuildAction de la imagen  a Resource y con el siguiente código mostrara la imagen durante 6 segundos

  1. System.Windows.SplashScreen splash = new System.Windows.SplashScreen(«LoadingScreen.png»);
  2.             splash.Show(false);
  3.             splash.Close(new TimeSpan(0, 0, 6));

ListView con color Alternativo

Una de las respuestas que he dado en los foros de MSDN es como crear un estilo para el control ListView que generase controles alternativos en cada fila, imitando los estilos de los grid antiguos. A mi no es algo que me guste especialmente pero aquí va la solución

 

  1. <Window.Resources>
  2.         <Style x:Key=»alternatingListViewItemStyle» TargetType=»{x:Type ListViewItem}»>
  3.             <Style.Triggers>
  4.               
  5.                 <Trigger Property=»ItemsControl.AlternationIndex» Value=»1″>
  6.                     <Setter Property=»Background» Value=»LightGray»></Setter>
  7.                 </Trigger>
  8.                 <Trigger Property=»ItemsControl.AlternationIndex» Value=»2″>
  9.                     <Setter Property=»Background» Value=»White»></Setter>
  10.                 </Trigger>
  11.             </Style.Triggers>
  12.           
  13.             <Setter Property=»Height» Value=»30″ />
  14.         </Style>
  15.     </Window.Resources>

Como podéis observar en el estilo se basa en Triggers y en la propiedad AlternationIndex

Ahora para utilizarla

  1. <ListView Name=»listTest» ItemContainerStyle=»{StaticResource alternatingListViewItemStyle AlternationCount=»2″ >
  2.             <ListView.View>
  3.                 <GridView>
  4.                     <GridViewColumn Header=»Nombre» />
  5.                     <GridViewColumn Header=»Apellido» />
  6.                     <GridViewColumn Header=»Dirección» />
  7.                 </GridView>
  8.             </ListView.View>
  9.             <!– Whatever you might have in here –>
  10.         </ListView>

Debéis fijaros que además de asignar el estilo debemos de poner la propiedad AlternationCount a 2 y ya esta

Utilizando JavaScript con el WebBrowser de WPF (2/2)

En el anterior Post vimos como llamar desde nuestra página web a metodos de nuestra aplicación WPF, en este vamos a ver como podemos inyectar JavaScript, Css nuevos y llamar al JavaScript que hemos inyectado.

Para el ejemplo vamos a utilizar este Javascript que nos resalta de amarillo en la pagina Web  las palabras que pasemos por parámetros.

Para todos los pasos lo que se va a utilizar es el objeto DOM de la pagina, vamos a ver como podemos manejarlo de cualquier manera ya que tenemos acceso total a el.

Lo primero que tenemos que hacer es inyectar el script, para ello utilizaremos el evento LoadCompleted del navegador.

  1. void browser_LoadCompleted(object sender, System.Windows.Navigation.NavigationEventArgs e)
  2. {
  3.    HTMLDocumentClass doc = browser.Document as HTMLDocumentClass;
  4.    IHTMLDocument2 doc2 = browser.Document as IHTMLDocument2;
  5.    originalContent = doc2.body.innerHTML;
  6.    //Creamos el script
  7.    IHTMLScriptElement script = (IHTMLScriptElement)doc2.createElement(«SCRIPT»);
  8.    script.type = «text/javascript»;
  9.    //función de javascript
  10.    script.text = hilightScript;

En este código obtenemos el documento que tenemos en la pagina, creamos el script a través del método createElement y lo asignamos

Ahora vamos a insertar un nuevo estilo css

  1.   IHTMLStyleSheet style = (IHTMLStyleSheet)doc2.createStyleSheet(«», 0);
  2.             style.cssText = «.hl {color:#f00;background-color:#ff0;font-weight:bold;}»;

Con esas dos líneas hemos creado un nuevo estilo en la página, ahora solo falta llamarla, el paso mas complicado

 

  1. IHTMLElementCollection nodes = doc.getElementsByTagName(«head»);
  2.   foreach (IHTMLElement elem in nodes)
  3.   {
  4.      //Añadir el script
  5.      HTMLHeadElementClass head = (HTMLHeadElementClass)elem;
  6.     head.appendChild((IHTMLDOMNode)script);
  7. }
  8.   
  9. //llamarlo
  10. var wholeScript = doc2.Script;
  11. wholeScript.GetType().InvokeMember(
  12. «hilite_injected», BindingFlags.InvokeMethod, null, wholeScript, new object[] { «prueba,google» });

Y con esto llamaríamos al javascript que hemos inyectado, realmente sencillo interactuar con las paginas que mostremos en nuestro webbrowser.

Utilizando JavaScript con el WebBrowser de WPF (1/2)

Con la llegada del FrameWork 3.5 SP1 apareció un nuevo control denominado WebBrowser que al igual que su hermano de WinForms es un ActiveX de IE que nos permite tener un navegador dentro de nuestras aplicaciones WPF. Al igual que su hermano de WinForms este es muy sencillo a la hora de utilizarlo, tiene el método Navigate donde indicamos la URL que queremos mostrar, ningún misterio hasta ahora. Las funciones soportadas por este control son:

  • NavigateToString
  • NavigateToStream
  • Navigate
  • GoBack
  • GoForward

Pero muchas veces o en mi caso me ha ocurrido es necesario interactuar desde la aplicación de WPF con la aplicación Web que alojamos en nuestro WebBrowser y la única manera de hacerlo (o por lo menos la que yo se) es a través de Javascript, invocando métodos de JavaScript desde  WPF o llamando a métodos de clases de WPF desde JavaScript.

Para realizar esto debemos de crear una clase en nuestra aplicación con el atributo [ComVisible(true)] que será el que interactué con la pagina. Como siempre se explica mejor con un ejemplo allá va.

Nuestro XAML va a ser muy sencillo, solo tendrá el objeto WebBrowser

  1. <Grid x:Name=»root»>
  2.         <WebBrowser x:Name=»browser» HorizontalAlignment=»Left»/>
  3.     </Grid>

La clase que tendrá los métodos que serán llamados desde JavaScript

  1. [ComVisible(true)]
  2.     public class ScriptingHelper
  3.     {
  4.         public void ShowMessage(string message)
  5.         {
  6.             MessageBox.Show(message);
  7.         }
  8.     

Como podéis ver solo tiene un método que muestra un mensaje al usuario, aparte de tener el atributo [ComVisible(true)] , los métodos deben de ser públicos.

Lo siguiente es navegar a la pagina que queremos, que en nuestro ejemplo es una página que creamos nosotros con un método JavaScript que será llamado desde un botón y este método JavaScript llamara al método de la clase WPF

  1. <!DOCTYPE HTML PUBLIC «-//W3C//DTD HTML 4.0 Transitional//EN»>
  2. <html>
  3. <head>
  4.     <title></title>
  5.     <script type=»text/javascript»>
  6.         function OnClick() {
  7.             var message = «Hola!»;
  8.             window.external.ShowMessage(message);
  9.         }
  10.     </script>
  11. </head>
  12. <body>
  13.   <a href=»#» onclick=»OnClick()»>Click Hola</a>
  14. </body>
  15. </html>

Si os fijáis la manera de llamarle es a través de window.external.NombreMetodo. Pero para que esto funcione debemos hacer que nuestro navegador navegue hasta esta pagina

  1. string uri = AppDomain.CurrentDomain.BaseDirectory + «TestPage.html»;
  2.             this.browser.Navigate(new Uri(uri, UriKind.Absolute));
  3.             this.browser.ObjectForScripting = new ScriptingHelper();

pero aparte de la pagina a navegar es importante indicarle al WebBrowser cual va a ser la clase que va a ser invocada desde la pagina Web a traves de la propiedad ObjectForScripting.

Para llamar a una función JavaScript desde nuestra aplicación WPF es mucho mas sencillo ya que utilizaremos el método InvokeScript donde el primer parámetro es el nombre del método javascript y el segundo es un array de objetos donde pasaremos los parámetros del método javascript

  1. this.webBrowser.InvokeScript(«JavaScriptFunctionWithParameters», this.messageTextBox.Text);

Problema con el control Expander dentro de un ListBox

En los foros surgio la pregunta porque el control Expander dentro de una ListBox no lanzaba el evento SelectionChanged de la listbox para saber cuando se pulsaba, la razón es que el evento del control  Expander es un evento enrutado que tiene el comportamiento de Buble con lo que no sube hasta la ListBox. Esta es la respuesta pero aprovechando la circunstancia primero voy a explicar que es el control Expander y luego como solucionar el problema.

El control Expander es paecido al GroupBox pero nos va a permitir expandir/collapsar su contenido, deriba de HeaderedContentControl que proporciona la implementación base para todos los controles que incluyen contenido único y tienen un encabezado. Tiene la propiedad Header para enviar la cabecera y la propiedad Content donde pondremos el contenido de lo que queremos expandir, por ultimo la propiedad IsExpanded nos dice si esta expandido o colapsado.

Es del estilo

image

Con la imagen ya lo conocéis todos, volviendo al problema este expander esta dentro de una Listbox, concretamente el xaml es

<ScrollViewer>
                <ListBox SelectionChanged="ListBox_SelectionChanged" ItemsSource="{Binding}">
                    <ItemsControl.ItemTemplate >
                        <DataTemplate>
                            <Border>
                                <Expander >
                                    <Expander.Header>
                                        <TextBlock Text="{Binding Path=Name}"/>
                                    </Expander.Header>
                                    <Expander.Content>
                                        <StackPanel>
                                            <TextBlock Text="{Binding Path=Age}"/>
                                            <TextBlock Text="Line 2"/>
                                            <TextBlock Text="Line 3"/>
                                        </StackPanel>
                                    </Expander.Content>
                                </Expander>
                            </Border>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ListBox>
            </ScrollViewer>

 

La clase Persona

 public class Person { public string Name { get; set; } public int Age { get; set; } }

Cargamos la clase Persona

  ObservableCollection<Person> data = new ObservableCollection<Person>();
            data.Add(new Person { Name = "One", Age = 10 });
            data.Add(new Person { Name = "Two", Age = 20 });
            data.Add(new Person { Name = "Three", Age = 30 });
            Root.DataContext = data;

Y el resultado es el de arriba, pero el problema es que no se disparaba el evento SelectionChanged, para que se produzca debo de enlazar la propiedad IsExpanded con la propiedad IsSelected del ListBox con modo TwoWay de manera que cuando expanda se pondrá a true y la propiedad IsSelected de la ListBox también y lanzara el SelectionChanged. El XAML quedara

 <ScrollViewer>
                <ListBox SelectionChanged="ListBox_SelectionChanged" ItemsSource="{Binding}">
                    <ItemsControl.ItemTemplate >
                        <DataTemplate>
                            <Border>
                                <Expander IsExpanded="{Binding IsSelected,Mode=TwoWay, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ListBoxItem}}}">
                                    <Expander.Header>
                                        <TextBlock Text="{Binding Path=Name}"/>
                                    </Expander.Header>
                                    <Expander.Content>
                                        <StackPanel>
                                            <TextBlock Text="{Binding Path=Age}"/>
                                            <TextBlock Text="Line 2"/>
                                            <TextBlock Text="Line 3"/>
                                        </StackPanel>
                                    </Expander.Content>
                                </Expander>
                            </Border>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ListBox>
            </ScrollViewer>

 

Si queréis que el Expander que este expandido no se colapse automaticamente cuando seleccione otro debeis de poner en el ListBox la propiedad SelectionMode a Multiple

 

<ListBox SelectionMode="Multiple"

Restricciones de los controles WinForms en aplicaciones de WPF

Hemos visto en los anteriores posts como utilizar controles WinForms en aplicaciones WPF y al revés, pero al introducir controles WinForms en nuestras aplicaciones WPF debemos de tener en cuenta ciertos aspectos.

  • Windows Forms tiene unas limitaciones que WPF no tiene, por ejemplo al incluirlos en el control WindowsFormsHost no significa que podamos rotarlo, este control no lo soporta y no lo hace.

 

  • Windows Forms tiene un hwnd por cada form, en cambio WPF utiliza el mismo hwnd para todo su contenido, esto tiene el efecto que nustro control Windos Forms estara siempre en el Top de nuestra aplicación, por mucho que cambiamos el Z-Index este lo ignorara y se pondrá en el top.

 

  • WindowsFormsHost y ElementHost tiene la propiedad Child que es donde hosteamos nuestros controles, no es una colección, con lo que tenemos la restricción de un control por host.

 

  • WPF y Windows Forms tienen diferentes modelos de escalado, con lo que al realizar el escalado de nos podemos encontrar con sorpresas desagradables.

 

  • El foco trabaja de diferente manera en WPF y Windows Forms, de manera que si tenemos un control WinForm dentro de nuestro WindowsFormsHost y este control tiene el foco en un textbox, si minimizamos nuestra aplicación de WPF y la restauramos el foco lo tendrá el control WindowsFormsHost pero no el control WinForm, con lo que habremos perdido el foco del TextBox

 

  • La propiedad opacidad no trabaja en WindowsFormsHost .

 

  • Cuando mezclas Windows Forms y WPF te debes asegurar que ElementHost o WindowsFormsHost estan disposed o tendras memory leaaks. WPD dispose tus WindowsFormsHost cuando la aplicación se cierra.

Seguro que faltarán, así que como siempre todo el monte no es orégano