Search code examples
c#wpfdata-bindingdatatrigger

WPF Datatrigger and UI dispatcher


I am updating a property value with in a method call multiple times. My UI elements are bound to data properties and my viewModel has INotifyPropertychnaged implemented. I have data triggers set to update UI values. It works fine for the final property value at the end of the method call. However, I want to refresh UI based on property value change within method call. What is the best way to do it?

Code Behind:

private void Button_Click(object sender, RoutedEventArgs e)
{
OrderViewModel model = this.DataContext as OrderViewModel;
model.OrderStatus = OrderViewModel.OrderStatuses.Updating;

// Processing order logic 
// takes about few seconds, Here I want to update UI with Refreshing Icon inprogress.gif

model.OrderStatus = OrderViewModel.OrderStatuses.Updated;

}

XAML Code:

<Window.Resources>
        <BitmapImage x:Key="NotCreatedSource" UriSource="/Images/notcreated_20x20.png" />

        <Style TargetType="Image" x:Key="OrderLineItem">
            <Setter Property="Source" Value="{StaticResource NotCreatedSource}" />
            <Setter Property="Height" Value="32" />
            <Setter Property="Width" Value="24" />
            <Style.Triggers>
                <DataTrigger Binding="{Binding OrderStatus, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Value="Updated">
                    <Setter Property="Source" Value="/Images/checkmark_icon.png" ></Setter>
                </DataTrigger>
                <DataTrigger Binding="{Binding OrderStatus, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Value="Updating">
                    <Setter Property="Source" Value="/Images/inprogress.gif" ></Setter>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>

    <StackPanel>
        <StackPanel Margin="12,100,30,0" Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Top">
            <Image Style="{StaticResource OrderLineItem}" Name="ConnectionImage" />
        </StackPanel>
        <Button Width="250" Height="25" Margin="20" Click="Button_Click">Change status</Button>

    </StackPanel>

Model Class:

public class OrderViewModel : INotifyPropertyChanged
    {
        public OrderStatuses OrderStatus 
        {
            get
            {
                return _orderstatus;
            }
            set
            {
                _orderstatus = value;
                RaisePropertyChanged("OrderStatus");
            }
        }
        private OrderStatuses _orderstatus;

        public event PropertyChangedEventHandler PropertyChanged;
        public void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }

        public enum OrderStatuses
        {
            Updated = 1,
            Updating,
            NotCreated,
            Failed,
            NotAvailable
        }
    }

and in UI based on those 2 statuses I have Image icons next to order Control. It never shows Icon for Updating, But at the end of call it shows updated.

How do I show both statuses update on UI?


Solution

  • The problem is you're blocking the UI thread for processing and WPF won't render. The hackiest workaround is like the old Application.DoEvents in VB6: periodically call this.Dispatcher.Invoke with a no-op delegate during the processing.

    Slightly better--but still sidestepping the core issue--is to wrap the processing and final update in a delegate passed to this.Dispatcher.BeginInvoke. This is a somewhat hacky continuation.

    A logically clean way is to move the processing to the background via a Task (or thread or BackgroundWorker or any other async API) and complete the status update in a task continuation.

    OrderViewModel model = this.DataContext as OrderViewModel;
    model.OrderStatus = OrderViewModel.OrderStatuses.Updating;
    
    Task.Factory.StartNew(() => /* processing order logic */)
        .ContinueWith(t => model.OrderStatus = OrderViewModel.OrderStatuses.Updated);
    

    Depending on your .NET Framework version, you can use await instead of ContinueWith.