views:

148

answers:

3

I'm new to WPF and MVVM. I think this is a simple question. My ViewModel is performing an asynch call to obtain data for a DataGrid which is bound to an ObservableCollection in the ViewModel. When the data is loaded, I set the proper ViewModel property and the DataGrid displays the data with no problem. However, I want to introduce a visual cue for the user that the data is loading. So, using Blend, I added this to my markup:

        <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="LoadingStateGroup">
            <VisualState x:Name="HistoryLoading">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="HistoryGrid">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}"/>
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
            <VisualState x:Name="HistoryLoaded">
                <Storyboard>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)" Storyboard.TargetName="WorkingStackPanel">
                        <DiscreteObjectKeyFrame KeyTime="0" Value="{x:Static Visibility.Hidden}"/>
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>

I think I know how to change the state in my code-behind (something similar to this):

VisualStateManager.GoToElementState(LayoutRoot, "HistoryLoaded", true);

However, the place where I want to do this is in the I/O completion method of my ViewModel which does not have a reference to it's corresponding View. How would I accomplish this using the MVVM pattern?

+1  A: 

What I've done in the past is to declare an event in my VM to which the View subscribes. Then, when I want to indicate that the busy indicator should disappear, I raise the event inside the VM.

Eric
A property is better than an event for this.
Ray Burns
+2  A: 

The standard way of doing this is normally to have a property in your viewmodel (either dependency property or one participating in INotifyPropertyChanged) that would signify that data is loading - perhaps bool IsLoadingData or similar. You set it to true when you start loading, and set it to false when you are done.

Then you would bind a trigger or visual state to this property in the view, and use the view to describe how to present to the user that the data is loading.

This approach maintains the separation where the viewmodel is the logical representation the user's view, and does not need to participate in the actual display - or have knowledge of animations, visual states, etc.

To use a DataStateBehavior to change visual states based on a binding in Silverlight:

<TheThingYouWantToModify ...>
 <i:Interaction.Behaviors>
   <ei:DataStateBehavior Binding="{Binding IsLoadingData}" Value="true" TrueState="HistoryLoading" FalseState="HistoryLoaded" />
 </i:Interaction.Behaviors>
</TheThingYouWantToModify >
Philip Rieck
I like this approach. Sorry to be so ignorant, but how do you bind a visual state to a property?
Decker
I believe it's different between silverlight and wpf 4. However, in SL4 you can use the DataStateBehavior or a DataStateSwitchBehavior - not a lot of examples out there, but enough to get started. You name your visual states, and add interaction.behavior nodes to the target controls with a datastatebehavior to change to the named visual state triggered on a bound property.
Philip Rieck
+1  A: 

Hi,

You can do something like this :

XAML

<Window x:Class="WpfSOTest.BusyWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfSOTest"
    Title="BusyWindow"
    Height="300"
    Width="300">
<Window.Resources>
    <local:VisibilityConverter x:Key="VisibilityConverter" />
</Window.Resources>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Border Grid.Row="0">
        <Grid>
            <Border>
                <Rectangle Width="400"
                           Height="400"
                           Fill="#EEE" />
            </Border>
            <Border Visibility="{Binding IsBusy, Converter={StaticResource VisibilityConverter}}">
                <Grid>
                    <Rectangle Width="400"
                               Height="400"
                               Fill="#AAA" />
                    <TextBlock Text="Busy"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Center" />
                </Grid>
            </Border>
        </Grid>
    </Border>
    <Border Grid.Row="1">
        <Button Click="ChangeVisualState">Change Visual State</Button>
    </Border>
</Grid>

Code:

public partial class BusyWindow : Window
{
    ViewModel viewModel = new ViewModel();

    public BusyWindow()
    {
        InitializeComponent();

        DataContext = viewModel;
    }

    private void ChangeVisualState(object sender, RoutedEventArgs e)
    {
        viewModel.IsBusy = !viewModel.IsBusy;
    }
}

public class ViewModel : INotifyPropertyChanged
{
    protected Boolean _isBusy;
    public Boolean IsBusy
    {
        get { return _isBusy; }
        set { _isBusy = value; RaisePropertyChanged("IsBusy"); }
    }

    public ViewModel()
    {
        IsBusy = false;
    }

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

class VisibilityConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        switch (((Boolean)value))
        {
            case true:
                return Visibility.Visible;
        }

        return Visibility.Collapsed;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

----------------------- Updated code -----------------------

XAML

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Border Grid.Row="0">
        <Grid>
            <local:MyBorder IsBusy="{Binding IsBusy}">
                <Grid>
                    <TextBlock Text="{Binding IsBusy}"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Center" />
                </Grid>
            </local:MyBorder>
        </Grid>
    </Border>
    <Border Grid.Row="1">
        <Button Click="ChangeVisualState">Change Visual State</Button>
    </Border>
</Grid>

Template

<Style TargetType="{x:Type local:MyBorder}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:MyBorder}">
                <Border Name="RootBorder">
                    <Border.Background>
                        <SolidColorBrush x:Name="NormalBrush"
                                         Color="Transparent" />
                    </Border.Background>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup Name="CommonGroups">
                            <VisualState Name="Normal" />
                        </VisualStateGroup>
                        <VisualStateGroup Name="CustomGroups">
                            <VisualState Name="Busy">
                                <VisualState.Storyboard>
                                    <Storyboard>
                                        <ColorAnimation Storyboard.TargetName="NormalBrush"
                                                        Storyboard.TargetProperty="Color"
                                                        Duration="0:0:0.5"
                                                        AutoReverse="True"
                                                        RepeatBehavior="Forever"
                                                        To="#EEE" />
                                    </Storyboard>
                                </VisualState.Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <ContentPresenter />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Custom Element

[TemplateVisualState(GroupName = "CustomGroups", Name = "Busy")]
public class MyBorder : ContentControl
{
    static MyBorder()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(MyBorder), new FrameworkPropertyMetadata(typeof(MyBorder)));
    }

    public Boolean IsBusy
    {
        get { return (Boolean)GetValue(IsBusyProperty); }
        set { SetValue(IsBusyProperty, value); }
    }

    public static readonly DependencyProperty IsBusyProperty =
        DependencyProperty.Register("IsBusy", typeof(Boolean), typeof(MyBorder), new UIPropertyMetadata(IsBusyPropertyChangedCallback));

    static void IsBusyPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as MyBorder).OnIsBusyPropertyChanged(d, e);
    }

    private void OnIsBusyPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (Convert.ToBoolean(e.NewValue))
        {
            VisualStateManager.GoToState(this, "Busy", true);
        }
        else
        {
            VisualStateManager.GoToState(this, "Normal", true);
        }
    }
}
decyclone
This is an interesting technique. Thank you. I may go this route -- but I first want to see if I can somehow change the view's VisualState based on a change to a ViewModel property. I think that's the only way I can do animation -- like fade ins and outs based on the state.
Decker
I have used the visibility property here, but what you can do is in your custom control, create a dependency property of type boolean, bind the IsBusy property to that property and change the visual state in the control itself when the property changes to a specific value.
decyclone