Category Archives: WPF

WPF – Visual State Manager

Po długiej przerwie związanej z przeprowadzką, czas powrócić do prowadzenia blog’a i pisania artykułów. Mam nadzieję, że od dziś posty będą pojawiać się częściej i bardziej regularnie chociaż nie wszystko jeszcze idzie po mojej myśliUśmiech.

No to zaczynamy… Visual State Manager został wprowadzony w wersji 4.0 Wcześniej mogliśmy go spotkać w Silverlight lub WPF Toolkit. Służy do zmieniania wyglądu kontrolek na podstawie zdefiniowanych stanów. Zastosowanie jest podobne więc do triggerów jednak różnica polega na tym, że wyzwalacze odpalają się po zmianie właściwości kontrolki (lub na skutek zdarzenia), z kolei StateManager zawiera zdefiniowane stany, które programista może ręcznie uruchomić

Najpierw definiujemy grupy stanów (VisualStateGroup), potem konkretne stany a w nich StoryBoard określający zmianę wyglądu. Ponadto można również zdefiniować przejście z jednego stanu do drugiego ( opóźnienie).

W Internecie widziałem wiele przykładów pokazujących jak działa VisualStateManager. Szczerze mówiąc miałem problemy z ich zrozumieniem – moim zdaniem nie pokazywały one istoty mechanizmu . Z tego względu na blogu przedstawię maksymalnie uproszczony przykład zastosowania (typowo akademicki).

Zacznijmy od zdefiniowania pojedynczej grupy i stanu (przejścia ominiemy):

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="CommonStates">                    
        <VisualState x:Name="StateA">
            <Storyboard>
                <DoubleAnimation Storyboard.TargetName="button1" 
                Storyboard.TargetProperty="Width" To="50" />
            </Storyboard>
        </VisualState>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Najważniejsze w powyższym kodzie to zdefiniowanie stanu StateA, który zmienia szerokość kontrolki o nazwie button1. Definicja stanów zawsze przybiera postać animacji (storyboard). W przykładzie zostanie uruchomiona animacja zmniejszająca (lub zwiększająca) szerokość do 50. Możemy oczywiście definiować różne stany i w ten sposób programować proste workflow’y. Zmiana interfejsu użytkownika może zostać w bardzo prosty sposób zdefiniowana przez stany a następnie z poziomu kodu programista może sterować wyglądem interfejsu. W c# aby przejść do wskazanego stanu wystarczy:

VisualStateManager.GoToElementState((FrameworkElement)LayoutRoot, "StateA", true);

Pierwszy parametr określa kontrolkę w której zostały zdefiniowane stany, drugi to oczywiście nazwa konkretnego stanu a ostatni definiuje czy należy wykonać przejścia między stanami (w tym przypadku nie zdefiniowano żadnych). Na zakończenie tego przykładu warto wskazać miejsce w którym powinny zostać zdefiniowane stany. Jak widać z wywołania GoToElementState stany zostały zdefiniowane w kontrolce o nazwie LayoutRoot (Grid). Dla jasności wklejam całość kodu:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="VisualStateManager" Height="191" Width="337">
    <Grid x:Name="LayoutRoot">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="CommonStates">                    
                <VisualState x:Name="StateA">
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="button1" 
                            Storyboard.TargetProperty="Width" To="50" />
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <Button Width="100" Height="100"  x:Name="button1" Click="button1_Click" Margin="14,6,39,12"/>                    
    </Grid>
</Window>

Możemy zdefiniować również czas trwania przejścia. Przykład A:

<VisualStateGroup x:Name="CommonStates"  >
...
    <VisualStateGroup.Transitions>
        <VisualTransition GeneratedDuration="0:0:5"/>
    </VisualStateGroup.Transitions>
</VisualStateGroup>

Powyższy kod spowoduje, że przejście do każdego stanu będzie trwało 5 sekund. Ponadto możemy określić czas wymagany do przejścia ze stanu A do stanuB:

<VisualStateGroup x:Name="CommonStates"  >
...
    <VisualStateGroup.Transitions>
        <VisualTransition From="StateA" To="StateB" GeneratedDuration="0:1:0.1"/>
    </VisualStateGroup.Transitions>
</VisualStateGroup>

Na dobry początek chyba wystarczy. Nie jest to skomplikowany mechanizm ale chyba wciąż mało popularny. W WPF toolkit istnieje od dawna ale dopiero w wersji 4.0 został dodany jako standardowa klasa w .NET Framework.

Model-View-ViewModel a zdarzenia w WPF.

Wzorzec MVVM jest najpopularniejszym rozwiązaniem architektonicznym dla WPF. Ze względu na wbudowany mechanizm wiązań, programiści chętnie sięgają po ten wzorzec projektowy. Jednym z problemów jest podpięcie zdarzenia. W większości przypadków możemy powiązać komendę za pomocą właściwości Command. Co jednak w przypadku gdy potrzebujemy specyficzne zdarzenie np. OnMouseDown? WPF niestety nie udostępnia właściwości typu OnClickCommand – do dyspozycji mamy tylko OnClick zwracający EventHandler a nie ICommand.

Rozwiązaniem są zachowania – obiekty doczepiane do kontrolek które mogą zmieniać ich zachowanie. Behaviour to klasa mająca referencję do kontrolki. Można więc stworzyć behaviour który podczepia się pod zdarzenie a następnie wywołuje naszą komendę. Istnieje już gotowa biblioteka implementująca stosowny behaviour – Marlon  Grech prezentował ją na swoim blogu. Cały kod wraz z przykładem można ściągnąć właśnie z jego blogu a konkretnie z stąd. Dla osób którzy nie chcą ściągać całego dema  wklejam kod wymaganych klas (źródło Marlon Grech):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Markup;
using System.Windows;
using System.Windows.Input;

namespace AttachedCommandBehavior
{
    /// <summary>
    /// Defines the attached properties to create a CommandBehaviorBinding
    /// </summary>
    public class CommandBehavior
    {
        #region Behavior

        /// <summary>
        /// Behavior Attached Dependency Property
        /// </summary>
        private static readonly DependencyProperty BehaviorProperty =
            DependencyProperty.RegisterAttached("Behavior", typeof(CommandBehaviorBinding), typeof(CommandBehavior),
                new FrameworkPropertyMetadata((CommandBehaviorBinding)null));

        /// <summary>
        /// Gets the Behavior property. 
        /// </summary>
        private static CommandBehaviorBinding GetBehavior(DependencyObject d)
        {
            return (CommandBehaviorBinding)d.GetValue(BehaviorProperty);
        }

        /// <summary>
        /// Sets the Behavior property.  
        /// </summary>
        private static void SetBehavior(DependencyObject d, CommandBehaviorBinding value)
        {
            d.SetValue(BehaviorProperty, value);
        }

        #endregion

        #region Command

        /// <summary>
        /// Command Attached Dependency Property
        /// </summary>
        public static readonly DependencyProperty CommandProperty =
            DependencyProperty.RegisterAttached("Command", typeof(ICommand), typeof(CommandBehavior),
                new FrameworkPropertyMetadata((ICommand)null,
                    new PropertyChangedCallback(OnCommandChanged)));

        /// <summary>
        /// Gets the Command property.  
        /// </summary>
        public static ICommand GetCommand(DependencyObject d)
        {
            return (ICommand)d.GetValue(CommandProperty);
        }

        /// <summary>
        /// Sets the Command property. 
        /// </summary>
        public static void SetCommand(DependencyObject d, ICommand value)
        {
            d.SetValue(CommandProperty, value);
        }

        /// <summary>
        /// Handles changes to the Command property.
        /// </summary>
        private static void OnCommandChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CommandBehaviorBinding binding = FetchOrCreateBinding(d);
            binding.Command = (ICommand)e.NewValue;
        }

        #endregion

        #region CommandParameter

        /// <summary>
        /// CommandParameter Attached Dependency Property
        /// </summary>
        public static readonly DependencyProperty CommandParameterProperty =
            DependencyProperty.RegisterAttached("CommandParameter", typeof(object), typeof(CommandBehavior),
                new FrameworkPropertyMetadata((object)null,
                    new PropertyChangedCallback(OnCommandParameterChanged)));

        /// <summary>
        /// Gets the CommandParameter property.  
        /// </summary>
        public static object GetCommandParameter(DependencyObject d)
        {
            return (object)d.GetValue(CommandParameterProperty);
        }

        /// <summary>
        /// Sets the CommandParameter property. 
        /// </summary>
        public static void SetCommandParameter(DependencyObject d, object value)
        {
            d.SetValue(CommandParameterProperty, value);
        }

        /// <summary>
        /// Handles changes to the CommandParameter property.
        /// </summary>
        private static void OnCommandParameterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CommandBehaviorBinding binding = FetchOrCreateBinding(d);
            binding.CommandParameter = e.NewValue;
        }

        #endregion

        #region Event

        /// <summary>
        /// Event Attached Dependency Property
        /// </summary>
        public static readonly DependencyProperty EventProperty =
            DependencyProperty.RegisterAttached("Event", typeof(string), typeof(CommandBehavior),
                new FrameworkPropertyMetadata((string)String.Empty,
                    new PropertyChangedCallback(OnEventChanged)));

        /// <summary>
        /// Gets the Event property.  This dependency property 
        /// indicates ....
        /// </summary>
        public static string GetEvent(DependencyObject d)
        {
            return (string)d.GetValue(EventProperty);
        }

        /// <summary>
        /// Sets the Event property.  This dependency property 
        /// indicates ....
        /// </summary>
        public static void SetEvent(DependencyObject d, string value)
        {
            d.SetValue(EventProperty, value);
        }

        /// <summary>
        /// Handles changes to the Event property.
        /// </summary>
        private static void OnEventChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            CommandBehaviorBinding binding = FetchOrCreateBinding(d);
            //check if the Event is set. If yes we need to rebind the Command to the new event and unregister the old one
            if (binding.Event != null && binding.Owner != null)
                binding.Dispose();
            //bind the new event to the command
            binding.BindEvent(d, e.NewValue.ToString());
        }

        #endregion

        #region Helpers
        //tries to get a CommandBehaviorBinding from the element. Creates a new instance if there is not one attached
        private static CommandBehaviorBinding FetchOrCreateBinding(DependencyObject d)
        {
            CommandBehaviorBinding binding = CommandBehavior.GetBehavior(d);
            if (binding == null)
            {
                binding = new CommandBehaviorBinding();
                CommandBehavior.SetBehavior(d, binding);
            }
            return binding;
        }
        #endregion

    }

}

CommandBehaviourBinding:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;
using System.Reflection;
using System.Windows;

namespace AttachedCommandBehavior
{
    /// <summary>
    /// Defines the command behavior binding
    /// </summary>
    public class CommandBehaviorBinding : IDisposable
    {
        #region Properties
        /// <summary>
        /// Get the owner of the CommandBinding ex: a Button
        /// This property can only be set from the BindEvent Method
        /// </summary>
        public DependencyObject Owner { get; private set; }
        /// <summary>
        /// The command to execute when the specified event is raised
        /// </summary>
        public ICommand Command { get; set; }
        /// <summary>
        /// Gets or sets a CommandParameter
        /// </summary>
        public object CommandParameter { get; set; }
        /// <summary>
        /// The event name to hook up to
        /// This property can only be set from the BindEvent Method
        /// </summary>
        public string EventName { get; private set; }
        /// <summary>
        /// The event info of the event
        /// </summary>
        public EventInfo Event { get; private set; }
        /// <summary>
        /// Gets the EventHandler for the binding with the event
        /// </summary>
        public Delegate EventHandler { get; private set; }

        #endregion

        //Creates an EventHandler on runtime and registers that handler to the Event specified
        public void BindEvent(DependencyObject owner, string eventName)
        {
            EventName = eventName;
            Owner = owner;
            Event = Owner.GetType().GetEvent(EventName, BindingFlags.Public | BindingFlags.Instance);
            if (Event == null)
                throw new InvalidOperationException(String.Format("Could not resolve event name {0}", EventName));

            //Create an event handler for the event that will call the ExecuteCommand method
            EventHandler = EventHandlerGenerator.CreateDelegate(
                Event.EventHandlerType, typeof(CommandBehaviorBinding).GetMethod("ExecuteCommand", BindingFlags.Public | BindingFlags.Instance), this);
            //Register the handler to the Event
            Event.AddEventHandler(Owner, EventHandler);
        }

        /// <summary>
        /// Executes the command
        /// </summary>
        public void ExecuteCommand()
        {
            if (Command.CanExecute(CommandParameter))
                Command.Execute(CommandParameter);
        }

        #region IDisposable Members
        bool disposed = false;
        /// <summary>
        /// Unregisters the EventHandler from the Event
        /// </summary>
        public void Dispose()
        {
            if (!disposed)
            {
                Event.RemoveEventHandler(Owner, EventHandler);
                disposed = true;
            }
        }

        #endregion
    }
}

EventHandlerGenerator:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection.Emit;
using System.Reflection;

namespace AttachedCommandBehavior
{
    /// <summary>
    /// Generates delegates according to the specified signature on runtime
    /// </summary>
    public static class EventHandlerGenerator
    {
        /// <summary>
        /// Generates a delegate with a matching signature of the supplied eventHandlerType
        /// This method only supports Events that have a delegate of type void
        /// </summary>
        /// <param name="eventInfo">The delegate type to wrap. Note that this must always be a void delegate</param>
        /// <param name="methodToInvoke">The method to invoke</param>
        /// <param name="methodInvoker">The object where the method resides</param>
        /// <returns>Returns a delegate with the same signature as eventHandlerType that calls the methodToInvoke inside</returns>
        public static Delegate CreateDelegate(Type eventHandlerType, MethodInfo methodToInvoke, object methodInvoker)
        {
            //Get the eventHandlerType signature
            var eventHandlerInfo = eventHandlerType.GetMethod("Invoke");
            Type returnType = eventHandlerInfo.ReturnParameter.ParameterType;
            if (returnType != typeof(void))
                throw new ApplicationException("Delegate has a return type. This only supprts event handlers that are void");

            ParameterInfo[] delegateParameters = eventHandlerInfo.GetParameters();
            //Get the list of type of parameters. Please note that we do + 1 because we have to push the object where the method resides i.e methodInvoker parameter
            Type[] hookupParameters = new Type[delegateParameters.Length + 1];
            hookupParameters[0] = methodInvoker.GetType();
            for (int i = 0; i < delegateParameters.Length; i++)
                hookupParameters[i + 1] = delegateParameters[i].ParameterType;

            DynamicMethod handler = new DynamicMethod("", null,
                hookupParameters, typeof(EventHandlerGenerator));

            ILGenerator eventIL = handler.GetILGenerator();

            //load the parameters or everything will just BAM 🙂
            LocalBuilder local = eventIL.DeclareLocal(typeof(object[]));
            eventIL.Emit(OpCodes.Ldc_I4, delegateParameters.Length + 1);
            eventIL.Emit(OpCodes.Newarr, typeof(object));
            eventIL.Emit(OpCodes.Stloc, local);

            //start from 1 because the first item is the instance. Load up all the arguments
            for (int i = 1; i < delegateParameters.Length + 1; i++)
            {
                eventIL.Emit(OpCodes.Ldloc, local);
                eventIL.Emit(OpCodes.Ldc_I4, i);
                eventIL.Emit(OpCodes.Ldarg, i);
                eventIL.Emit(OpCodes.Stelem_Ref);
            }

            eventIL.Emit(OpCodes.Ldloc, local);

            //Load as first argument the instance of the object for the methodToInvoke i.e methodInvoker
            eventIL.Emit(OpCodes.Ldarg_0);

            //Now that we have it all set up call the actual method that we want to call for the binding
            eventIL.EmitCall(OpCodes.Call, methodToInvoke, null);

            eventIL.Emit(OpCodes.Pop);
            eventIL.Emit(OpCodes.Ret);

            //create a delegate from the dynamic method
            return handler.CreateDelegate(eventHandlerType, methodInvoker);
        }

    }
}

Kodu jak widać jest sporo. Wykorzystanie jednak jest już łatwe:

<Border Background="Aqua" 
local:CommandBehavior.Event="MouseDown" 
local:CommandBehavior.Command="{Binding SomeCommand}"
local:CommandBehavior.CommandParameter="{Binding}"/>

W powyższym przykładzie bindujemy komendę SomeCommand do zdarzenia MouseDown. Jako parametr zdarzenia otrzymamy stosowne EventArgs.

Ustawienie “Culture” w WPF

WPF mam jedną niedogodność – domyślnie wszelkie ustawienia regionalne interfejsu są ustawione na en-US. Oznacza to, że wszystkie domyślne konwertery również korzystają z ustawień en-US. Chcąc wyświetlić np. datę, prawdopodobnie pożądanym będzie przedstawienie daty polskiemu użytkownikowi w formacie dd-mm-yyyy oraz amerykańskiemu w mm-dd-yyyy. Niestety domyślnie właściwość UIElement::Language jest ustawiona na en-US.  Za pomocą prostego kodu możemy jednak zmienić metadane i ustawić prawidłową “kulturę”:

protected override void OnStartup(StartupEventArgs e)
{
    FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(System.Windows.Markup.XmlLanguage.GetLanguage(System.Threading.Thread.CurrentThread.CurrentCulture.IetfLanguageTag)));
    
    base.OnStartup(e);
}

Ustawienie prawidłowych ustawień regionalnych najlepiej umieścić w OnStartup ponieważ wystarczy to wykonać tylko raz – przy starcie aplikacji.

IntTextBox oraz RealTextBox w WPF

Poprawnie zaprojektowana aplikacja powinna weryfikować dane w każdej warstwie systemu. Oczywiście najważniejszym miejscem jest warstwa biznesowa ale dobrym zwyczajem jest walidacja również w warstwie prezentacji. W idealnym interfejsie użytkownik nie jest w stanie wprowadzić błędnych danych. Podstawowym przykładem są pola edycyjne w których powinno się wpisać np. ilość sprzedanego produktu. Użytkownik nie powinien mieć możliwości wprowadzenia tekstu w takie pole.

Zacznijmy od prostej sprawy – akceptacja tylko cyfr.

public class IntNumberTextBox:System.Windows.Controls.TextBox
{
    protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
    {
        e.Handled = !ValidateText(e.Text);
        base.OnPreviewTextInput(e);
    } 
    private bool ValidateText(string text)
    {
        foreach (Char character in text)
        {
            if (System.Char.IsDigit(character) == false)
                return false;
        }
        return true;
    }
}

Jak widać wystarczy tylko przeładować metodę OnPreviewTextInput i ustawić flagę Handled na true w przypadku gdy wprowadzony znak jest niedozwolony.

W przypadku liczb rzeczywistych musimy umożliwić dodatkowo wprowadzanie przecinków lub kropek. Znak oddzielający część dziesiętną od całkowitej zależy od ustawień regionalnych w systemie operacyjnym. W C# możemy sprawdzić jaki to jest znak za pomocą właściwości System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator.

public class RealNumberTextBox:System.Windows.Controls.TextBox
{
   protected override void OnPreviewTextInput(System.Windows.Input.TextCompositionEventArgs e)
   {
       e.Handled = !ValidateText(e.Text);
       base.OnPreviewTextInput(e);
   }
   private bool ValidateText(string text)
   {
       if ( text == System.Globalization.NumberFormatInfo.CurrentInfo.NumberDecimalSeparator)
                  return true;
       foreach (Char character in text)
       {
           if (System.Char.IsDigit(character) == false)
               return false;
       }
       return true;
   }
}

Inne ciekawe wartości, zależne od ustawień regionalnych, które są przydatne w walidacji to m.in CurrencyDecimalSeparator, CurrencyGroupSeparator, CurrencySymbol, NegativeSign, NegativeInfinitySymbol.

Teraz pozostało nam tylko wykorzystać nowo utworzoną kontrolkę w pliku XAML. Najpierw należy zadeklarować przestrzeń nazw:

<Window x:Class="Sample.Views.Product.Product"
        xmlns:controls="clr-namespace:Sampple.Controls"
        inne deklaracje
>

Następnie korzystamy z kontrolki w sposób analogiczny do standardowych kontrolek:

<controls:IntNumberTextBox HorizontalAlignment="Left" Width="200" Text="tekst" />

Pamiętajmy, że walidacja w dolnych warstwach jest zwykle wolniejsza ponieważ należy połączyć się najpierw np. z bazą danych lub z usługą sieciową. Ponadto korzystanie z aplikacji z walidacją w warstwie prezentacji jest po prostu łatwiejsze dla użytkownika.

Wymuszenie walidacji kontrolek w WPF

Windows Presentation Foundation posiada nowy model walidacji kontrolek. Każdej kontrolce podlegającej walidacji przypisujemy zestaw reguł walidacyjnych, np:

<TextBox  Height="23" Margin="120,6,177,0"  VerticalAlignment="Top" >
    <Binding Path="FirstName" Mode="TwoWay">
        <Binding.ValidationRules>
            <DataErrorValidationRule></DataErrorValidationRule>
        </Binding.ValidationRules>
    </Binding>
</TextBox>

Powyższa reguła mówi, że wykorzystywany obiekt biznesowy wspiera interfejs IDataErrorInfo:

public partial class Person : IDataErrorInfo
{
   private Dictionary<string, string> m_ValidationErrors = new Dictionary<string, string>();
   public Invoice()
   {       
   }
   private void AddError(string columnName, string msg)
   {
       if (!m_ValidationErrors.ContainsKey(columnName))
       {
           m_ValidationErrors.Add(columnName, msg);
       }
   }
   private void RemoveError(string columnName)
   {
       if (m_ValidationErrors.ContainsKey(columnName))
       {
           m_ValidationErrors.Remove(columnName);
       }
   }
   partial void OnFirstNameChanged()
   {
       if (string.IsNullOrEmpty(ReceiverName))
       {
           AddError("FirstName", "Musisz podac imie");
       }
       else
           RemoveError("FirstName");
   }
   
   #region IDataErrorInfo Members

   public string Error
   {
       get
       {
           if (m_ValidationErrors.Count > 0)
           {
               return m_ValidationErrors.ElementAt(0).Value;
           }
           else return null;
       }
   }
   public string this[string columnName]
   {
       get
       {
           if (m_ValidationErrors.ContainsKey(columnName))
           {
               return m_ValidationErrors[columnName];
           }
           else return null;
       }
   }
   #endregion
}

W przykładzie wykorzystałem obiekt Entity Framework. Implementacje wymusza nam interfejs więc myślę, że kod nie wymaga dodatkowego komentarza(jeśli macie jakieś pytania to proszę zadawać je np. w komentarzach).

Inna regułą, która spełnia się gdy zostanie wyjątek wyrzucony podczas próby zmiany właściwości jest ExceptionValidationRule. Ponadto, istnieje możliwość definiowania własnych reguł. Gdy któraś kontrolka nie przejdzie walidacji zostaje domyślnie podświetlona na czerwono:

image

Walidacja zostanie jednak wywoływana pierwszy raz w momencie gdy kontrolka straci focus bądź gdy zostanie zmieniona w niej wartość(zależy to od ustawień bindingu). Co jednak w przypadku gdy chcemy aby przy starcie okna błędnie wypełnione pola(w tym przykładzie puste) zostały zaznaczone na czerwono?

Rozwiązanie jest proste: Wystarczy obsłużyć zdarzenie Windows Loaded oraz wykonać poniższy kod:

m_FirstName.GetBindingExpression(TextBox.TextProperty).UpdateSource();

Po wykonaniu kodu, wszystkie puste pola(czyli te, które nie przeszły walidacji) będą podświetlone i tym samym użytkownik będzie wiedział co należy wypełnić.

Aktualizacja interfejsu z drugiego wątku(windows forms i WPF)

Na różnych forach często użytkownicy mają problem z aktualizacją kontrolek z innego wątku. Załóżmy, że odpaliliśmy sobie BackgroundWorker lub po prostu Thread. Wykonujemy jakieś operację, np. łączymy się ze zdalnymi zasobami. Stworzenie osobnego wątku w takim scenariuszu jest bardzo pożądane ponieważ nie blokujemy wtedy aktualizacji interfejsu. W trakcie pobierania informacji z Internetu chcemy aktualizować interfejs aby informować użytkownika o postępach np.

progressBar.Value = progessValue;

Jeśli powyższy kod jest wywołany z obcego wątku użytkownik dostanie następujący komunikat o błędzie:

Cross-thread operation not valid:
Control accessed from a thread other than the thread it was created on.

Wyjątek jest wyrzucany ponieważ zarówno w WindowsForms jak i w WPF nie można aktualizować interfejsu z innego wątku niż z tego w którym została stworzona kontrolka. Musimy więc w jakiś sposób dostać się do wątku macierzystego dla kontrolki i tam wykonać aktualizacje tej kontrolki. Służy do tego metoda Control.Invoke(windows forms) lub ControlDispatcher.Invoke(Wpf). Chcąc więc zaktualizować ProgressBar w WinForms musimy napisać:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
progressBar.Invoke(updateAction,32);

Powyższy kod będzie działał w każdym przypadku ale ma jedną wadę związaną z wydajnością kodu. Co w przypadku gdy mamy osobną klasę do aktualizacji interfejsu i jest ona wywoływana zarówno z wątku kontrolki jak i z obcego wątku? Dla drugiego przypadku(z obcego wątku) kod jest maksymalnie optymalny. Z kolei w sytuacji gdy wywołujemy ją z wątku macierzystego dla kontrolki nie potrzebnie będzie wykonywana masa operacji związanych z wpompowaniem operacji w kolejkę dla wątku interfejsu. Na szczęście istnieje gotowa metoda, która stwierdza czy dla aktualnego przypadku użycia jest wymagane wywołanie Invoke:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
if (progressBar.InvokeRequired)
    progressBar.Invoke(updateAction,5);
else
    updateAction(4);

Posiadamy już optymalną wersje kodu. Jednak pisanie powyższego kodu za każdym razem gdy chcemy zaktualizować interfejs(nigdy nie wiemy czy kod nie będzie wywoływany w przyszłości z innego wątku) jest co najmniej niewygodne. Z ratunkiem przychodzą nam tzw. rozszerzenia(extensions,c# 3.0). Możemy przecież stworzyć własną metodę nazwaną powiedzmy InvokeIfRequired:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (control.InvokeRequired)
           control.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (control.InvokeRequired)
           control.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Za pomocą takiego rozwiązania aktualizacja kontrolki sprowadzi się wyłącznie do poniższego kodu:

this.InvokeIfRequired((value) => progressBar.Value = value, 10);

W przypadku Wpf rozwiązanie jest bardzo podobne:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Z kolei użycie metody jest identyczne jak w przypadku WinForms:

this.InvokeIfRequired((value) => bar.Value = value, 10);