ProgressBar en una ventana WPF
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.
La situación de ejemplo es la siguiente:
Suponemos un cronometro inverso, que cuente los segundos que faltan para que acabe un proceso. De esta forma podremos usar los segundos que faltan para actualizar la propiedad [value] del ProgressBar.
Este problema existe también con los demás controles que muestran información, Label, TextBox, etc, ya que se comportan de la misma manera que el ProgressBar. Si queremos actualizarlos TAMBIÉN tendremos que usar hilos
En este ejemplo mostramos en un control TextBox, una cadena que indica en minutos y segundos el tiempo que falta para que se acabe el proceso
Cuando se ejecuta el programa Muestra una ventana con tres botones [Aceptar], [Cancelar],[Terminar] una barra de progreso y un TextBox (dentro de la barra) que muestra el tiempo que falta
Imagen de la pantalla de este formulario XAML

Código XAML
<Window x:Class="WindowPruebaProgressBarConTask" 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" xmlns:local="clr-namespace:PausadorNet2023" mc:Ignorable="d" Title="Window Prueba ProgressBar Con Task" 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 de la clase Value Objecto que mueve los datos
Esta clase se usa para mover informacion entre el subproceso Task y el formulario
El codigo de la misma esta en este enlace
Código Visual Basic .Net
' /** ' --------------------------------------------- ' [- (A) Contenido -] ' --------------------------------------------- ' ------------- ' [- Descripción -] = ' 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, ' he incluido dentro del control (ProgressBar). ' ------------- ' [- Observaciones -] = ' Para evitar que el formulario se bloquee usamos la clase [System.Threading.Tasks.Task]. ' 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)]. ' Para mover la información entre el subproceso [Task] y el formulario XAML, usamos una case [Value Object] llamada [DatosDevueltos]. ' Esta clase contiene dos propiedades de lectura:. ' a) El numero de segundos que falta para que se termine el proceso, (recibidos en el constructor). ' b) y el texto, una cadena formateada con los minutos y segundos correspondientes al numero de segundos. ' Por ejemplo recibe 200 segundos y devuelve la cadena "03:20". ' Es decir 200 segundos son 3 minutos y 20 segundos. ' El valor numérico de los segundos se usa en la propiedad value del PorgressBar. ' La cadena se emplea en texto dentro del ProgressBar. ' El 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. ' ------------- ' [- Bibliografía -] = https://foxlearn.com/windows-forms/update-paramProgress-bar-from-async-task-in-csharp-352.html?utm_content = cmp-true. ' --------------------------------------------- ' [- /Eof -] ' */ '-------------------------------------------------------- ' Necesario para la clase [CancellationTokenSource] Imports System.Threading ' Necesario para la clase Task Imports System.Threading.Tasks Public Class WindowPruebaProgressBarConTask #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 '---------------------------------------------------- Call DisposeTark() End Sub Private Sub BotonCloseCommandBinding_CanExecute(sender As Object, e As CanExecuteRoutedEventArgs) ' e.CanExecute = True e.CanExecute = localBotonTerminarActivado End Sub #End Region #Region "Proceso Task" ''' <summary> ''' Para el informe de progreso. ''' Proporciona un objeto IProgress(Of T) que invoca las devoluciones de llamada para cada valor de progreso notificado. ''' </summary> Private progress As New Progress(Of DatosDevueltos)(Sub(ByVal paramObjDatosVO As DatosDevueltos) pbStatus.Value = paramObjDatosVO.NumeroDeSegundos TextBoxCronometro.Text = paramObjDatosVO.TextoTiempoRestante End Sub) ' Define el token de cancelación. ' Señala un objeto CancellationToken que debe cancelarse ' Para controlar la posible cancelación de la operación, ' se crea una instancia de un objeto CancellationTokenSource ' que genera un token de cancelación que se pasa a un objeto TaskFactory. ' A su vez, el objeto TaskFactory pasa el token de cancelación ' a cada una de las tareas que se ejecuten en segundo plano. Private sourceCancellationToken As New CancellationTokenSource() 'Propaga la notificación de que las operaciones deberían cancelarse. Private tokenCancellation As CancellationToken = sourceCancellationToken.Token ''' <summary> ''' Pone en marcha el proceso asíncrono ''' </summary> Private Async Sub ArrancarTareaAsicrona() '---------------------------------------------------- ' valores para el paramProgress 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 '-------------------------- ' Instanciar los objetos para que funcionen cada vez que se pulse [Ejecutar] sourceCancellationToken = New CancellationTokenSource() tokenCancellation = sourceCancellationToken.Token '-------------------------- 'EjecutarTask se ejecuta en el grupo de subprocesos. Await System.Threading.Tasks.Task.Run(Function() Return EjecutarTask(progress, tokenCancellation) End Function) 'TextBoxCronometro.Text = "Terminado!" End Sub ''' <summary> ''' El proceso que realiza el trabajo en el hilo ''' En este caso cuenta segundos ''' </summary> ''' <param name = "paramProgress">Un objeto IProgress(Of T) que se usara para hacer notificaciones.</param> ''' <param name = "paramTokenCancelacion">Propaga la notificación de que las operaciones deberían cancelarse.</param> ''' <returns> ''' Devuelve un valor lógico Integer (porque así lo he decidido) ''' Return = 0 --> False, Ha habido algún error, el proceso no se ha completado ''' Return = 1 --> True, Todo correcto, el proceso se ha completado sin problemas ''' </returns> Private Function EjecutarTask(ByVal paramProgress As IProgress(Of DatosDevueltos), ByVal paramTokenCancelacion As CancellationToken) As Integer '--------------------------- ' En este programa (de prueba de concepto) se simula un contador de milisegundos ' la constante [DuracionSegundos] contiene el numero de segundos que hay que esperar ' Se actualiza la barra de progreso cada segundo ' El bucle For-Next cuenta el número de segundos que se esperan '--------------------------- For i As Integer = DuracionSegundos To 0 Step -1 '------------------------------------- ' la cancelación del proceso If Not (paramTokenCancelacion = Nothing) Then ' [IsCancellationRequested] Obtiene la información ' sobre si se ha cancelado el proceso (True) o no (False) If paramTokenCancelacion.IsCancellationRequested = True Then Return 0 ' False 'Exit For End If End If '------------------------------------- ' simular una segundo de espera ' un segundo = 1000 Milisegundos Thread.Sleep(MILISEGUNDOS_DE_ESPERA) '------------------------------------- ' informe de progreso If paramProgress IsNot Nothing Then ' Cargar la información en la clase [DatosDevueltos] ' y devolver esa clase a través del objeto [IProgress] Dim objDatosVO As New DatosDevueltos(i) paramProgress.Report(objDatosVO) End If Next 'valor que se devuelve Return 1 ' true End Function Private Sub CancelarTareaAsincrona() '-------------------------- ' Situación de los botones localBotonEjecurarActivado = True localBotonCancelarActivado = False localBotonTerminarActivado = True sourceCancellationToken.Cancel() End Sub Public Sub DisposeTark() '---------------------------------------------------- 'cerrar la ventana '---------------------------------------------------- ' Primero destruimos los objetos usados If progress IsNot Nothing Then progress = Nothing End If If sourceCancellationToken IsNot Nothing Then sourceCancellationToken = Nothing End If If tokenCancellation <> Nothing Then tokenCancellation = Nothing End If '---------------------------------------------------- ' Después cerramos la ventana Me.Close() End Sub #End Region End Class