En el moderno XAML, el control ProgressBar no funciona como lo hacia en los formularios. el problema es que el código que mueve el valor del control ProgressBar esta en otro hilo, y por razones de seguridad no permite su actualización, lo que ocurre es que aparentemente no pasa nada hasta que acaba el proceso y entonces aparece el control ProgressBar totalmente lleno
Este comportamiento se puede solucionar de varias formas pero todas ellas implican trabajar con hilos.
En este ejemplo se resuelve ese problema usando la clase BackgroundWorker, pero aunque funciona, esta un poco obsoleta y, tal y como recomienda MSDN hay que usar la clase Task
Imagen del programa funcionando
Código XAML de la ventana
<Window x:Class="WindowPruebaProgressBarConBackgroundWorker" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" Title="Window Prueba ProgressBar Con BackgroundWorker" Height="150" Width="500" ResizeMode="CanResizeWithGrip"> <Window.Resources> <!-- ++++++++++++++++++++++++++++++++++++++++++ --> <!-- Comandos personalizados para los botones inferiores [Ejecutar], [Cancelar], [Terminar] --> <!--El comando personalizado para el botón [Aceptar] --> <RoutedUICommand x:Key="RCBotonEjecutarCmd" Text="Acciones que se producen al pulsar el botón [Aceptar] del Grupo de botones [Ejecutar], [Cancelar], [Terminar]"> </RoutedUICommand> <!--El comando personalizado para el botón [Cancelar] --> <RoutedUICommand x:Key="RCBotonCancelarCmd" Text="Acciones que se producen al pulsar el botón [Cancelar] del Grupo de botones [Ejecutar], [Cancelar], [Terminar]"> </RoutedUICommand> </Window.Resources> <Window.CommandBindings> <!-- ++++++++++++++++++++++++++++++++++++++++++ --> <!-- Los botones inferiores / Ejecutar / Cancelar / Salir --> <!--El comando personalizado para el botón [Ejecutar] --> <CommandBinding Command="{StaticResource RCBotonEjecutarCmd}" Executed="BotonEjecutarCommandBinding_Executed" CanExecute="BotonEjecutarCommandBinding_CanExecute"/> <!--El comando personalizado para el botón [Cancelar] --> <CommandBinding Command="{StaticResource RCBotonCancelarCmd}" Executed="BotonCancelarCommandBinding_Executed" CanExecute="BotonCancelarCommandBinding_CanExecute"/> <!--El comando personalizado para el botón [Salir / Terminar] --> <CommandBinding Command="ApplicationCommands.Close" Executed="BotonCloseCommandBinding_Executed" CanExecute="BotonCloseCommandBinding_CanExecute"/> </Window.CommandBindings> <Window.InputBindings> <!-- Atajo de teclado para que se ejecute Botón [Salir] --> <KeyBinding Key="Esc" Modifiers="" Command="ApplicationCommands.Close" /> <KeyBinding Key="F4" Modifiers="Alt" Command="ApplicationCommands.Close" /> <!-- Atajo de teclado para que se ejecute Botón [Ejecutar] / [Cancelar] --> <KeyBinding Key="F5" Modifiers="" Command="{StaticResource RCBotonEjecutarCmd}" /> <KeyBinding Key="F5" Modifiers="Ctrl" Command="{StaticResource RCBotonCancelarCmd}" /> </Window.InputBindings> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="*"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Grid Grid.Row="0" x:Name="GridProgressBarConPorcentaje" HorizontalAlignment="Stretch" VerticalAlignment="Top" Height="25" Margin="0,10,0,0" > <ProgressBar x:Name="pbStatus" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Value="{Binding Value, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" Foreground="#FF09F5BA" > <ProgressBar.Background> <ImageBrush/> </ProgressBar.Background> </ProgressBar> <!--<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="NoWrap" FontFamily="Consolas" Text="{Binding Value, ElementName=pbStatus, StringFormat=\{0:00.00\} %}" />--> <TextBlock x:Name="TextBoxCronometro" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="Consolas" TextWrapping="Wrap" Text="Cancelado!" Width="75" Opacity="0.5"/> </Grid> <!-- Grid con los tres botones [Ejecutar], [Cancelar],[ Terminar] --> <Grid Grid.Row="2" Name="GridPanelTresBotonesOkCancelSalirComImagenes" HorizontalAlignment="Center" VerticalAlignment="Bottom" Grid.IsSharedSizeScope="True" Margin="0,10,10,0"> <Grid.ColumnDefinitions> <ColumnDefinition x:Uid="ColumnDefinition1" SharedSizeGroup="ButtonsAceptarCancelar" Width="120"/> <ColumnDefinition x:Uid="ColumnDefinition3" SharedSizeGroup="ButtonsAceptarCancelar" /> <ColumnDefinition x:Uid="ColumnDefinition3" SharedSizeGroup="ButtonsAceptarCancelar" /> </Grid.ColumnDefinitions> <Button Grid.Column="0" Name="ButtonEjecutar" Command="{StaticResource RCBotonEjecutarCmd}" IsCancel="False" IsEnabled="true" > <Button.ToolTip> <StackPanel Orientation="Vertical"> <TextBlock Text=" Ejecutar" /> <TextBlock Text=" Ejecuta (pone en marcha) el proceso" /> <TextBlock Text=" Atajo de teclado [F5] " /> </StackPanel> </Button.ToolTip> <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center"> <!-- VS2019 Image Library / Status Run --> <Viewbox Width="16" Height="16" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Rectangle Width="16" Height="16"> <Rectangle.Fill> <DrawingBrush> <DrawingBrush.Drawing> <DrawingGroup> <DrawingGroup.Children> <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M0,8C0,3.582 3.582,0 8,0 12.418,0 16,3.582 16,8 16,12.418 12.418,16 8,16 3.582,16 0,12.418 0,8" /> <GeometryDrawing Brush="#FF329932" Geometry="F1M6,12L6,4 12,8z M8,1C4.135,1 1,4.134 1,8 1,11.865 4.135,15 8,15 11.865,15 15,11.865 15,8 15,4.134 11.865,1 8,1" /> <GeometryDrawing Brush="#FFFFFFFF" Geometry="F1M6,4L12,8 6,12z" /> </DrawingGroup.Children> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Rectangle.Fill> </Rectangle> </Viewbox> <Label Content="Ejecutar" /> </StackPanel> </Button> <Button Grid.Column="1" Name="ButtonCancelar" Command="{StaticResource RCBotonCancelarCmd}" IsCancel="False" IsEnabled="True"> <Button.ToolTip> <StackPanel Orientation="Vertical"> <TextBlock Text=" Cancelar" /> <TextBlock Text=" Detiene el proceso y vuelve a la situación inicial " /> <TextBlock Text=" Los datos internos de trabajo se pierden" /> <TextBlock Text=" Para detener la ejecución del programa primero" /> <TextBlock Text=" se pulsa el botón [Cancelar] y a continuación [Salir]" /> <TextBlock Text=" Atajo de teclado [Ctrl + F5] " /> </StackPanel> </Button.ToolTip> <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center"> <!-- VS2019 Image Library / Status Stop --> <Viewbox Width="16" Height="16" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Rectangle Width="16" Height="16"> <Rectangle.Fill> <DrawingBrush> <DrawingBrush.Drawing> <DrawingGroup> <DrawingGroup.Children> <GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M0,8C0,3.582 3.582,0 8,0 12.418,0 16,3.582 16,8 16,12.418 12.418,16 8,16 3.582,16 0,12.418 0,8" /> <GeometryDrawing Brush="#FFE41400" Geometry="F1M11,11L5,11 5,5 11,5z M8,1C4.135,1 1,4.134 1,8 1,11.865 4.135,15 8,15 11.865,15 15,11.865 15,8 15,4.134 11.865,1 8,1" /> <GeometryDrawing Brush="#FFFFFFFF" Geometry="F1M11,11L5,11 5,5 11,5z" /> </DrawingGroup.Children> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Rectangle.Fill> </Rectangle> </Viewbox> <Label Content="Cancelar" /> </StackPanel> </Button> <Button Grid.Column="2" Name="ButtonTerminar" Command="ApplicationCommands.Close" IsCancel="True" IsEnabled="True" > <Button.ToolTip> <StackPanel Orientation="Vertical"> <TextBlock Text=" Salir" /> <TextBlock Text=" Termina la ejecución del programa " /> <TextBlock Text=" y cierra esta ventana" /> <TextBlock Text=" Atajo de teclado [Alt + F4] " /> <TextBlock Text=" Atajo de teclado [Esc] " /> </StackPanel> </Button.ToolTip> <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center"> <!-- VS2019 Image Library / Exit --> <Viewbox Width="16" Height="16" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <Rectangle Width="16" Height="16"> <Rectangle.Fill> <DrawingBrush> <DrawingBrush.Drawing> <DrawingGroup> <DrawingGroup.Children> <GeometryDrawing Brush="#00FFFFFF" Geometry="F1M16,16L0,16 0,0 16,0z" /> <GeometryDrawing Brush="#FFF6F6F6" Geometry="F1M9.5859,10L8.9999,10.586 8.9999,11.445C8.4099,11.789 7.7319,12 6.9999,12 4.7909,12 2.9999,10.209 2.9999,8 2.9999,5.791 4.7909,4 6.9999,4 7.7319,4 8.4099,4.211 8.9999,4.555L8.9999,5.414 9.5859,6 7.9999,6 7.9999,10z M13.2279,4.813C12.0669,2.551 9.7169,1 6.9999,1 3.1339,1 -9.99999999997669E-05,4.134 -9.99999999997669E-05,8 -9.99999999997669E-05,11.866 3.1339,15 6.9999,15 9.7169,15 12.0669,13.449 13.2279,11.187L15.9999,8.414 15.9999,7.586z" /> <GeometryDrawing Brush="#FF414141" Geometry="F1M7,13C4.238,13 2,10.762 2,8 2,5.238 4.238,3 7,3 8.118,3 9.14,3.38 9.973,4L11.463,4C10.365,2.775 8.775,2 7,2 3.686,2 1,4.687 1,8 1,11.313 3.686,14 7,14 8.775,14 10.365,13.225 11.463,12L9.973,12C9.14,12.62,8.118,13,7,13" /> <GeometryDrawing Brush="#FF414141" Geometry="F1M12,5L10,5 12,7 9,7 9,9 12,9 10,11 12,11 15,8z" /> </DrawingGroup.Children> </DrawingGroup> </DrawingBrush.Drawing> </DrawingBrush> </Rectangle.Fill> </Rectangle> </Viewbox> <Label Content="Salir" /> </StackPanel> </Button> </Grid> <!-- /Eof Grid con los tres botones [Ejecutar], [Cancelar],[ Terminar] --> </Grid> </Window>
Código VB De la calse VO (Value Object)
El código Visual Basic de esta clase esta en este enlace
Código VB
'--------------------------------------------------------------------------------------------------------------- ' Este formulario (WPF) es un estudio de como se puede usar un control [Barra de progreso (ProgressBar)] ' en un formulario WPF y que el control se actualice durante el proceso sin que la barra deje de funcionar ' modificando el comportamiento básico. ' En este ejemplo suponemos que representamos un contador de tiempo descendente ' para ello usamos una [Barra de progreso (ProgressBar)] y un [Cuadro de texto (TextBox)] que indicara con un texto cuanto tiempo falta , ' y que Para hacerlo mas bonito, hemos incluido dentro del control (ProgressBar) ' ' Para evitar que el formulario se bloquee usamos la clase [System.ComponentModel.BackgroundWorker] ' Para simular el paso del tiempo usamos un bucle For-Next que contiene el numero de segundos que queremos que dure el proceso ' Para simular los segundos, dentro de cada iteración del bucle For-Next esperamos exactamente 1 segundo entre cada iteración ' y para ello usamos la instrucción [System.Threading.Thread.Sleep(1000)] ' ' Ademas la función [CalculoTextoCronometro] recibe un numero de segundos (en formato integer) ' y devuelve una cadena formateada con los minutos y segundos correspondientes ' Por ejemplo recibe 200 segundos y devuelve la cadena "03:20" ' Es decir 200 segundos son 3 minutos y 20 segundos ' Esta cadena se emplea como texto dentro del ProgressBar ' ' ' Eel bucle que cuenta el tiempo, lo cuenta al revés, es decir quedan 50 segundos 49, 48, etc ' de forma que la representación de los valores en los controles no es igual, ' la barra de progreso cuenta a delante, (han pasado un segundo, 2,3,etc ' y el texto muestra el tiempo que queda ( quedan 20 segundos, 19, 18, etc) ' lo que obliga a algún cambio de valores iniciales. en realidad no es ningún problema y se ve fácilmente si estas avisado ' ' El código esta muy (excesivamente) documentado, para que cuando vuelva a necesitarlo, sepa lo que hace cada parte y por que '--------------------------------------------------------------------------------------------------------------- ' necesario para usar [BackgroundWorker] Imports System.ComponentModel Class WindowPruebaProgressBarConBackgroundWorker #Region ''' <summary> ''' El numero de segundos que estará activa la ventana ''' </summary> ''' <value> ''' Un valor integer que representa el numero de segundos que se mostrara la ventana ''' </value> Public Property DuracionSegundos As Integer = 70I ''' <summary> ''' Tiempo que se espera en el bucle para simular un segundo ''' El valor que tiene que tener esta contaste es de 1000 milisegundos ''' (O sea 1 segundo) ''' pongo un valor mas pequeño para pruebas ''' Cambiar a 1000 para esperar un segundo entre iteraciones ''' </summary> Private Const MILISEGUNDOS_DE_ESPERA As Integer = 100 #End Region #Region "Eventos Command de los botones [Aceptar], [Cancelar], [Terminar]" Private localBotonEjecurarActivado As Boolean = True Private localBotonCancelarActivado As Boolean = False Private localBotonTerminarActivado As Boolean = True Private Sub BotonEjecutarCommandBinding_Executed(sender As Object, e As ExecutedRoutedEventArgs) '---------------------------------------------------- 'Arrancar la operación asincrónica. Call ArrancarTareaAsicrona() End Sub Private Sub BotonEjecutarCommandBinding_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs) ' e.CanExecute = True e.CanExecute = localBotonEjecurarActivado End Sub Private Sub BotonCancelarCommandBinding_Executed(sender As Object, e As ExecutedRoutedEventArgs) '---------------------------------------------------- 'Cancelar la operación asincrónica. Call CancelarTareaAsincrona() End Sub Private Sub BotonCancelarCommandBinding_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs) ' e.CanExecute = True e.CanExecute = localBotonCancelarActivado End Sub Private Sub BotonCloseCommandBinding_Executed(sender As Object, e As ExecutedRoutedEventArgs) '---------------------------------------------------- 'cerrar la ventana '---------------------------------------------------- ' Primero destruimos los objetos usados If Not (MyWorker Is Nothing) Then MyWorker.Dispose() MyWorker = Nothing End If '---------------------------------------------------- ' Después cerramos la ventana Me.Close() End Sub Private Sub BotonCloseCommandBinding_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs) ' e.CanExecute = True e.CanExecute = localBotonTerminarActivado End Sub #End Region #Region "[BackgroundWorker]" ''' <summary> ''' El objeto [BackgroundWorker] ''' </summary> ''' <remarks> ''' Se declara [WithEvents] para poder usar sus eventos ''' - DoWork: realiza el trabajo real de la aplicación ''' - ProgressChanged: actualiza el valor real de la barra de progreso. ''' - RunWorkerCompleted: se dispara al terminar el progreso, ''' </remarks> Private WithEvents MyWorker As New System.ComponentModel.BackgroundWorker() ''' <summary> ''' Función que coordina los valores de los botones del ''' formulario y arranca la tarea [BackgroundWorker] ''' </summary> Private Sub ArrancarTareaAsicrona() '---------------------------------------------------- ' valores para el progress bar que mueve este código pbStatus.Maximum = DuracionSegundos pbStatus.Minimum = 0 pbStatus.Value = 0 '-------------------------- ' Situación de los botones localBotonEjecurarActivado = False localBotonCancelarActivado = True localBotonTerminarActivado = False '---------------------------------------------------- ' Establece el valor que indica que BackgroundWorker ' SI puede crear informes sobre las actualizaciones de progreso. MyWorker.WorkerReportsProgress = True '---------------------------------------------------- 'Establece el valor que indica BackgroundWorker 'SI admite la cancelación asincrónica. MyWorker.WorkerSupportsCancellation = True '---------------------------------------------------- 'Inicia la ejecución de una operación en segundo plano. Call MyWorker.RunWorkerAsync() End Sub ''' <summary> ''' Cancela la tarea asíncrona ''' </summary> Private Sub CancelarTareaAsincrona() '---------------------------------------------------- 'Cancelar la operación asincrónica. Me.MyWorker.CancelAsync() '---------------------------------------------------- ' Situación de los botones localBotonEjecurarActivado = True localBotonCancelarActivado = False localBotonTerminarActivado = True End Sub '-------------------------------------------------------------------------- ' ' DoWork: realiza el trabajo real de la aplicación (como cargar datos) y, ' siempre que sea posible, informa del progreso a la barra de progreso ' mediante su método ReportProgress. ' Esto desencadenará el evento ProgressChanged. ' 'ProgressChanged: actualiza el valor real de la barra de progreso. ' Este evento acepta un parámetro para que pueda pasar ' cantidades variables, dependiendo de la carga de trabajo. ' 'RunWorkerCompleted: termina con el progreso, ' suele ocultar la barra de progreso o mostrar un mensaje al usuario. '-------------------------------------------------------------------------- ''' <summary> ''' DoWork: realiza el trabajo real de la aplicación (como cargar datos) y, ''' siempre que sea posible, informa del progreso a la barra de progreso ''' mediante su método ReportProgress. ''' Esto desencadenará el evento ProgressChanged. ''' </summary> Private Sub MyWorker_DoWork(sender As Object, e As DoWorkEventArgs) Handles MyWorker.DoWork For i As Integer = DuracionSegundos To 0 Step -1 '------------------------------------------------------ ' Cancela la operación si el usuario la ha cancelado. ' Tenga en cuenta que una llamada a CancelAsync puede haber configurado ' CancelaciónPendiente a verdadero justo después del ' La última invocación de este método sale, por lo que esto ' el código no tendrá la oportunidad de configurar el ' DoWorkEventArgs.Cancel marca a verdadero. Esto significa ' que RunWorkerCompletedEventArgs.Cancelled ' no debe establecerse en verdadero en su RunWorkerCompleted ' controlador de eventos. Esta es una condición de carrera. '------------------------------------------------------ If MyWorker.CancellationPending Then e.Cancel = True Else TryCast(sender, System.ComponentModel.BackgroundWorker).ReportProgress(i) ' -------------------------------------------- ' Esperar un segundo ' System.Threading.Thread.Sleep(1000) ' refactorizado ' -------------------------------------------- System.Threading.Thread.Sleep(MILISEGUNDOS_DE_ESPERA) End If Next End Sub ''' <summary> ''' ProgressChanged: actualiza el valor real de la barra de progreso. ''' Este evento acepta un parámetro para que pueda pasar ''' cantidades variables, dependiendo de la carga de trabajo. ''' </summary> ''' <param name = "sender"></param> ''' <param name = "e"> ''' contiene el valor numérico del numero de segundos con los que se trabaja en este momento ''' este valor se recibe del valor de bucle [For-Next] que esta en el evento [DoWork] ''' </param> ''' <remarks> ''' En este programa en concreto, se calculan dos valores diferentes, ''' y es en esta función donde se calculan los valores auxiliares y ''' donde se actualizan en los controles del formulario XAML ''' ----------------------- ''' A) Por un lado un valor numérico (Obtenido a través del parámetro [e.ProgressPercentage]) ''' que se supone que son el numero de segundos que faltan para que la operación se complete ''' y que es el que se usa para actualizar el [progress bar] con su propiedad Value ''' B) En segundo lugar, ese numero que actualiza la barra de progreso, ''' se pasan a la función [CalculoTextoCronometro] cuyo trabajo es convertir ese número en ''' una cadena de tiempo formateada. ''' Por ejemplo: la función [CalculoTextoCronometro] ''' recibe por parámetros 200 (segundos) y devuelve la cadena "03:20" ''' ''' Esta cadena se utiliza en el texto que esta dentro de la [barra de progreso] ''' y que indica cuanto tiempo falta para que termine la tarea ''' ----------------------- ''' </remarks> Private Sub MyWorker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles MyWorker.ProgressChanged '------------------------------------------------ ' El objeto VO que mueve la información entre el hilo y el formulario ' No es necesario usar una clase, el código puede incluirse en el formulario ' pero como lo uso también para Task, he decidido incluirlo en una clase Dim ObjDatos As New DatosDevueltos(e.ProgressPercentage) ' A) Valor [Value] de la [barra de progreso] pbStatus.Value = ObjDatos.NumeroDeSegundos ' B) Texto que se muestra con la [barra de progreso] Me.TextBoxCronometro.Text = ObjDatos.TextoTiempoRestante ' End Sub ''' <summary> ''' RunWorkerCompleted: termina con el progreso, ''' Suele usarse para ocultar la barra de progreso o para mostrar un mensaje al usuario. ''' </summary> Private Sub MyWorker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles MyWorker.RunWorkerCompleted If e.Cancelled = True Then TextBoxCronometro.Text = "Cancelado!" pbStatus.Value = 0 ElseIf e.Error IsNot Nothing Then TextBoxCronometro.Text = "Error: Else TextBoxCronometro.Text = "Hecho!" End If '---------------------------------------------------- ' Situación de los botones localBotonEjecurarActivado = True localBotonCancelarActivado = False localBotonTerminarActivado = True End Sub #End Region End Class