State handling in WPF-MVVM

I’m going to present an example of three states in a classical view-model.

3states

Notice that this repository contains a more complete demo, with multiple levels and a working copy&paste functionality.
Ok, we’ll start from a TreeView like below

		<TreeView                    ItemsSource="{Binding TreeItems}"  		  Grid.Column="0" Grid.Row="0" HorizontalAlignment="Stretch"                    VerticalAlignment="Stretch" Margin="38,26,32,32"                          Width="220" Height="180"                     SelectedItemChanged="Tree_SelectedItemChanged" Name="tree">
			<TreeView.Resources>
<Style TargetType="TreeViewItem"> <Setter Property="IsSelected" Value="{Binding Selected, Mode=TwoWay}" />
				</Style>

			</TreeView.Resources>
			<TreeView.ItemTemplate>
				<HierarchicalDataTemplate  					ItemsSource="{Binding TreeItems}">
					<TextBlock 						Text="{Binding Name}" />
					<HierarchicalDataTemplate.ItemTemplate>
						<DataTemplate>
							<StackPanel 								Orientation="Horizontal">
								<TextBlock 									Text="{Binding Name}" />
								<CheckBox 									IsEnabled="False" 									IsChecked="{Binding Selected}" />
							</StackPanel>
						</DataTemplate>
					</HierarchicalDataTemplate.ItemTemplate>
				</HierarchicalDataTemplate>
			</TreeView.ItemTemplate>
		</TreeView>

and a simple code behind

	public partial class Window1 : Window
	{
		TreeViewModel tvModel = new TreeViewModel();
		public Window1()
		{
			InitializeComponent();
			DataContext = tvModel;
		}

		void Tree_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
		{
			if (hidden.Text.Equals("Choices")) {
				opBox.Focus();
				hidden.Text = "";
			}
		}
	}

Now it’s time to see the view-model

		private enum CutViewState {
			Normal = 0,
			Cut = 1,
			Paste = 2
		}
		string cutFrom;
		string pasteTo;
		private CutViewState viewState;

		internal void notifySel() {

			OnPropertyChanged("SelectedItem");

			if (selectedItem.Name.Equals("Choices") || !selectedItem.Selected) {
				return;
			}

			StateText = "Selected: " + selectedItem.Name;
			switch (viewState) {
				case TreeViewModel.CutViewState.Normal:

					cutFrom = selectedItem.Name;
					viewState = CutViewState.Cut;
					Operation = "Select a node to paste to";
					break;
				case TreeViewModel.CutViewState.Cut:
					if (selectedItem.Name.Equals(cutFrom)) {
						break;
					}
					viewState = CutViewState.Paste;
					pasteTo = selectedItem.Name;
					Operation = "Done! From " + cutFrom + " to " + pasteTo  ;
					break;
				case TreeViewModel.CutViewState.Paste:
					viewState = CutViewState.Normal;
					Operation = "Select a node to cut";
					HiddenStatus = "Choices";
					break;
				default:
					throw new Exception("Invalid value for CutViewState");
			}
		}

with an internal TreeItem model

		ObservableCollection<TreeItem> treeItems = new ObservableCollection<TreeItem>();
		public  ObservableCollection<TreeItem> TreeItems {
			get { return treeItems; }
			set {
				treeItems = value;
			}
		}

		private string name;
		public string Name {
			get { return name; }
			set {
				name = value;
				OnPropertyChanged("Name");
			}

		}
		internal TreeViewModel model;
		private bool selected;
		public bool Selected {
			get { return selected; }
			set {
				selected = value;
				OnPropertyChanged("Selected");
				if (model != null) {
					model.selectedItem = this;
					model.notifySel();
				}
			}
		}

Notice in particular that you can apply the Visitor pattern to define the behavior of each state.
The core function is completely simplified.

 internal void notifySel() {

    OnPropertyChanged("SelectedItem");

    if (!selectedItem.Selected) {
       return;
    }

    StateText = "Selected: " + selectedItem.Name;

    viewState.accept(cutVisitor);
 }

All the logic is moved in the visitor

class CutViewVisitor : ICutViewVisitor
    {

        override internal void visit(PasteState state)
        {
            state.model.viewState = state.model.normalState;
            state.model.Operation = "Select a node to cut";
            state.model.HiddenStatus = "Choices";
        }

        override internal void visit(NormalState state)
        {
            if (state.model.selectedItem.Name.Equals("Choices"))
            {
                return;
            }
            state.model.cutFrom = state.model.selectedItem.Name;
            state.model.fromParent = state.model.selectedItem.parent;
            state.model.viewState = state.model.cutState;
            state.model.Operation = "Select a node to paste to";
        }

        override internal void visit(CutState state)
        {
            if (state.model.selectedItem.Name.Equals(state.model.cutFrom))
            {
                return;
            }
            state.model.viewState = state.model.pasteState;
            state.model.pasteTo = state.model.selectedItem.Name;
            if (state.model.fromParent != null)
            {
                var foundFrom = state.model.fromParent.TreeItems.First(t => t.Name.Equals(state.model.cutFrom));
                if (foundFrom == null)
                {
                    state.model.Operation = "Error! From " + state.model.cutFrom + " to " + state.model.pasteTo;
                }
                else
                {
                    state.model.fromParent.TreeItems.Remove(foundFrom);
                    state.model.selectedItem.TreeItems.Add(foundFrom);
                    foundFrom.parent = state.model.selectedItem;
                    state.model.Operation = "Done! From " + state.model.cutFrom + " to " + state.model.pasteTo;
                }

            }
            else
            {
                state.model.Operation = "Error! From " + state.model.cutFrom + " to " + state.model.pasteTo;
            }
        }

    }
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s