XAML.cz Magazín moderních technologií založených na XAML

Coproject – demo RIA aplikace krok za krokem, díl 9.

Napsáno pro Silverlight od Augustin Šulc [22.02.2011]

V tomto díle přidáme do Coprojectu toolbar a uložíme data do databáze.

Toolbar

Téměř každá aplikace má toolbar, případně jiný způsob, jak uživateli zpřístupnit nějaké akce. Problémem však bývá, že tento toolbar je umístěn na jiném místě, než samotné view, kterého se týká. Zde však opět využijeme vlastnost Caliburn.Micro, která nám umožňuje připojit více views k jednomu view modelu. Atribut, který rozlišuje, jaké view se použije, se nazývá Context.

Otevřete tedy solution Coprojectu z minulého dílu, nebo si stáhněte zdrojové kódy z webu Codeplexu a přidejte tento prvek do kořenového gridu v ToDoListsView:

<ContentControl Grid.Column="1" Grid.Row="0" Margin="10,0,0,0" HorizontalContentAlignment="Stretch" 
				cal:View.Model="{Binding ActiveItem}" cal:View.Context="Toolbar" />

Důležitý je druhý řádek, který určuje, že se obsah prvku má bindovat na property ActiveItem (nezapomeňte, že jeden prvek jménem ActiveItem tam již máme – jde o vlastní obsah detailu ToDoItem) a zároveň, že místo výchozího view má být použito view z kontextu Toolbar.

Postup, jakým C.M hledá view pro určitý kontext je následující: vezme výchozí view (např. Views.ToDoItemView), usekne “View” na konci (pokud se vyskytuje), přidá tečku a název kontextu. V našem případě bude tedy pro toolbar hledat view Views.ToDoItem.Toolbar. Proto si vytvoříme takové view a původní obsah (Grid s názvem LayoutRoot) nahradíme tímto:

<Border Style="{StaticResource FilterPanelStyle}">
	<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
		<Button x:Name="Edit" Content="Edit" Margin="0,0,5,0" />
		<Button x:Name="Cancel" Content="Cancel" Margin="0,0,5,0" />
		<Button x:Name="Save" Content="Save" Margin="0,0,5,0" />
		<Button x:Name="TryClose" Content="Close" />
	</StackPanel>
</Border>

Projekt by tedy měl vypadat asi takto:

Kontextové view jsou bindovány va view model stejným způsobem jako běžné view, je tedy jasné, že teď budeme implementovat funkce Edit, Cancel a Save (TryClose je již obsaženo v třídě Screen).

View model

View model s detailem by se měl chovat tak, že po otevření je v módu pouze pro čtení a tlačítka Cancel a Save jsou disablována. Pokud uživatel klikne na Edit, dojde k přepnutí do editačního módu. K řízení toho, v jakém módu se detail nachází, si vytvořím property IsReadOnly. Otevřete tedy ToDoItemViewModel a přidejte do něj property:

private bool _isReadOnly;
public bool IsReadOnly
{
	get
	{
		return this._isReadOnly;
	}
	set
	{
		this._isReadOnly = value;
		NotifyOfPropertyChange(() => IsReadOnly);
	}
}

Následuje velice jednoduchá implementace funkce Edit:

public void Edit()
{
	IsReadOnly = false;
}

Funkce Cancel bude trochu složitější, jelikož musíme vrátit případné uživatelem provedené změny v entitě. K tomu využijeme toho, že entity z RIA Services implementují IEditableObject. Situaci ještě trochu zkomplikuje i DataForm, který využíváme pro editaci dat – obsahuje chybu, která způsobuje špatné přepínání do ReadOnly módu. Musíme proto přepnout do ReadOnly módu, pak zpět do editačního módu a nakonec zpátky do ReadOnly.

public void Cancel()
{
	(Item as IEditableObject).CancelEdit();
	IsReadOnly = true;
	#region Fix DataForm bug
	IsReadOnly = false; IsReadOnly = true;
	#endregion
}

K ukládání dat budeme potřebovat nějaký result (opět se nám připomínají Coroutines), v tomto případě půjde o obdobu LoadDataResultu vytvořeného dříve. Do složky Framework si přidejte novou třídu jménem SaveDataResult:

namespace Coproject.Framework
{
	public class SaveDataResult : IResult
	{
		public DomainContext Context { get; private set; }
		public SubmitOperation Result { get; private set; }

		public event EventHandler<ResultCompletionEventArgs> Completed;

		public SaveDataResult(DomainContext context)
		{
			Context = context;
		}

		public void Execute(ActionExecutionContext context)
		{
			Context.SubmitChanges(SaveDataCallback, null);
		}

		private void SaveDataCallback(SubmitOperation data)
		{
			Result = data;
			OnCompleted();
		}

		private void OnCompleted()
		{
			var handler = Completed;
			if (handler != null)
			{
				handler(this, new ResultCompletionEventArgs());
			}
		}
	}
}

S využitím této třídy již bude implementace funkce ToDoItemViewModel.Save() celkem přehledná:

public IEnumerable<IResult> Save()
{
	(Item as IEditableObject).EndEdit();
	IsReadOnly = true;
	#region Fix DataForm bug
	IsReadOnly = false; IsReadOnly = true;
	#endregion

	yield return new SaveDataResult(_context);
}

Teď už jen stačí nastavit DataForm v ToDoItemView, aby bral v úvahu property IsReadOnly:

<dataForm:DataForm CurrentItem="{Binding Item}" IsReadOnly="{Binding IsReadOnly}" HorizontalAlignment="Stretch">

Když teď spustíte aplikaci, bude přepínání mezi módy i ukládání fungovat (jen musíte po uložení znovu načíst seznam). Máme však dvě drobné chybky - hned po otevření detailu jsme rovnou v editačním módu a všechny tlačítka jsou stále aktivní. První problém se dá snadno opravit – na začátek funkce Setup(int toDoItemID) vložte tento kód:

IsReadOnly = true;

Guard funkce

K vyřešení druhého problému použijeme takzvané “guard” funkce. Jde o další konvenci Caliburn.Micro – pokud je ovládací prvek navázán na nějakou akci a view model zároveň obsahuje funkci nebo property typu bool s názvem CanXXX kde XXX je jméno navázané akce, ovládací prvek bude aktivní pouze pokud bude tato “Can” funkce/property vracet kladnou hodnotu. Jak snadno uhodnete, budeme muset ve view modelu implementovat funkce CanEdit, CanCancel a CanSave:

public bool CanEdit
{
	get { return IsReadOnly; }
}
		
public bool CanCancel
{
	get { return !IsReadOnly; }
}

public bool CanSave
{
	get { return Item.HasChanges && !Item.HasValidationErrors; }
}

Jelikož se jedná o property, musíme nějak dát vědět, že se jejich hodnota změnila. A protože všechny tři závisejí na property IsReadOnly, musíme její část upravit takto:

set
{
	this._isReadOnly = value;
	NotifyOfPropertyChange(() => IsReadOnly);
	NotifyOfPropertyChange(() => CanEdit);
	NotifyOfPropertyChange(() => CanCancel);
}

Property CanSave také závisí na stavu editovaného prvku, přidejte proto na konec funkce Setup tento řádek:

Item.PropertyChanged += (s, e) => NotifyOfPropertyChange(() => CanSave);

Spusťte aplikaci a vyzkoušejte, že v každé chvíli jsou aktivní pouze ta tlačítka, která mají smysl. Nebylo to snadné?

Kontrola neuložených změn

Tak nějak “mimochodem” nám již funguje také tlačítko Close. K tomu se ale váže jeden celkem běžný požadavek na funkcionalitu – při pokusu o zavření okna je potřeba zjistit, zda neobsahuje neuložené změny, a pokud obsahuje, zeptáme se uživatele, zda si opravdu přeje okno zavřít. K tomu využijeme virtuální funkci CanClose, kterou C.M volá před tím, než se pokusí zavřít daný view model. Je na funkci, jak si s touto situací poradí, až se ale rozhodne, zda je možno opravdu view model zavřít, je nutné zavolat akci skrytou v parametru callback. Přidejte tedy tuto funkci do ToDoItemViewModelu:

public override void CanClose(Action<bool> callback)
{
	if (Item.HasChanges)
	{
		var result = MessageBox.Show("Current item was not saved. Do you really want to close it?", 
			"Coproject", MessageBoxButton.OKCancel);
		callback(result == MessageBoxResult.OK);
	}
	else
	{
		callback(true);
	}
}

Pozn.: V hotové aplikaci jistě nepoužijete dialogové okno s tlačítky OK a Storno jako v tomto případě a místo toho nejspíše využijete vlastní dialogové okno.

Na závěr bych rád přidal ještě jednu funkci – pokud zavřeme okno s neuloženými změnami, měli bychom tyto změny nejprve vrátit stejně, jako to dělá funkce Cancel. Ani tady nás nečeká náročné programování:

protected override void OnDeactivate(bool isClosing)
{
	if (isClosing && CanCancel)
	{
		Cancel();
	}
}

UpdateSourceOnChange

Jistě jste si všimli, že při upravování textboxu Content musíte nejprve kliknout někam mimo prvek, abyste aktivovali tlačítko Save. Je to způsobeno tím, že Silverlight aktualizuje binding u textboxů ne při změně textu, ale až ve chvíli, kdy textbox ztratí focus. Narozdíl od WPF navíc nemůžete toto chování změnit nastavením UpdateSourceTrigger=PropertyChanged. V Silverlightu je potřeba si trochu pomoci – vytvořte složku Helpers a do ní přidejte třídu BindingHelper:

public static class BindingHelper
{
	public static bool GetUpdateSourceOnChange(DependencyObject obj)
	{
		return (bool)obj.GetValue(UpdateSourceOnChangeProperty);
	}

	public static void SetUpdateSourceOnChange(DependencyObject obj, bool value)
	{
		obj.SetValue(UpdateSourceOnChangeProperty, value);
	}

	public static readonly DependencyProperty UpdateSourceOnChangeProperty =
		DependencyProperty.RegisterAttached("UpdateSourceOnChange", typeof(bool), 
		typeof(BindingHelper), new PropertyMetadata(false, OnPropertyChanged));

	private static void OnPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
	{
		var textBox = obj as TextBox;
		if (textBox != null)
		{
			if ((bool)e.NewValue)
			{
				textBox.TextChanged += TextBox_TextChanged;
			}
			else
			{
				textBox.TextChanged -= TextBox_TextChanged;
			}
		}
	}

	private static void TextBox_TextChanged(object sender, TextChangedEventArgs e)
	{
		var textBox = sender as TextBox;
		if (textBox != null)
		{
			var binding = textBox.GetBindingExpression(TextBox.TextProperty);
			if (binding != null)
			{
				binding.UpdateSource();
			}
		}
	}
}

Struktura projektu pak bude vypadat takto:

Teď už jen stačí tuto třídu použít. Otevřete tedy ToDoItemView a nejprve do něj zaregistrujte tento namespace:

xmlns:helpers="clr-namespace:Coproject.Helpers"

DataField vázaný na Content (nachází se v DataFormu) pak upravte takto:

<TextBox Text="{Binding Content,Mode=TwoWay}" TextWrapping="Wrap" Height="auto"
			helpers:BindingHelper.UpdateSourceOnChange="True"/>

A to je vše – spusťte aplikaci.

 

Diskuze

Jak jste zatím spokojeni se seriálem? Čekáte na českou verzi, nebo jste si již přečetli originální verzi v angličtině? Pomohl vám v něčem? Máte návrhy na další témata? Jak jste problémy, které jsme zatím probrali, řešili dosud?

Komentáře

ukládám komentář, vyčkejte prosím..
  1. Petr Balat

    Pěknej článek;-)

    použím wpf s caliburn a mám problém s clickonce + binding. Pokud aplikaci spouštím lokálně tak binding (TextBox, Combobox) funguje, pokud ale aplikaci spouštím z clickonce binding z viewModelu přestane fungovat. Full trust security nepomohlo:-(

    Nevíte náhodou co s tím?

    23.02.2011 @ 11:25
  2. Tak s tímhle jsem se zatím nesetkal, ale je to docela divné. Nezdá se ani, že by s tím měl někdo jiný problém (diskuze a podobně). Asi bych zkusil debug těch bindovacích funkcí Caliburnu, pokud to jde, a případně vyzkoušet novou verzi Caliburnu (včera vyšla 2.0), případně Caliburn.Micro (1.0).

    23.02.2011 @ 12:00

@xamlcz

  • RT @jvanrhyn: XAML, It's a bit like olives. Takes a while to get used to. But once you're used to it. It is actually pretty good. <3 XAML
  • RT @moser_christian: WPF Inspector 0.9.7 is released. It supports .NET 3.5 and 4.0 The project is now open source and available on CodeP ...
  • Jeff Handley oznámil vydání WCF RIA Services v.1.0 SP1 RTM http://bit.ly/gOgckn ke stažení na http://bit.ly/gVAXdK
  • jedna výzva pro Brno. Byl někdo z vás na přednášce o RIA v MS Akvárku? Dejte o sobě vědět. Děkuji
  • také jste uvažovali o tom, že zkusíte na projekt použít Caliburn Micro nebo naopak Prism 4? A co tak obojí, šlo by to nebo ne? Již brzy