čtvrtek 12. prosince 2013

Implementace události VisibilityChanged v javascriptu

Potřebovali jste někdy dostat informaci ve chvíli, kdy se změní viditelnost nějakého prvku na stránce? Já k podobnému problému došel zrovna nedávno a nenašel žádné vhodné řešení, tak s pomocí pár tipů na fóru stackoverflow nabízím vlastní.

Budeme potřebovat knihovnu jquery (v příkladu používám verzi 1.10.2) a její šikovný selector ":visible". Tento selektor umí rozpoznat jestli element, ke kterému se váže je skutečně vidět - tzn. není to je tupá kontrola css-kové vlastnost visibility, ale projde si i všechny rodičovské elementy v DOMu, jestli náhodou nemá vidtelnost vypnutou některý z nich.

Následně budeme kontrolovat v pravidelném intervalu stav daného elementu (nebo více elementů), aktuální hodnotu viditelnosti si uložíme do "data-" atributu a v jakmile dojde ke změně vystřelíme událost.

Vytvoříme si obslužnou třídu VisibilityChangedObserver, která bude mít public metody
  1. register($selector, callback)
    - zaregistruje událost visibilityChanged na zadaném prvku/prvcích dle použitého selektoru a zavolá callback s parametrem event (jquery event object), který v propertě result nese booleanovou hodnotu jestli je cílová element viditelný
  2. deregister($selector)
    - odregistruje událost visibilityChanged na zadaném prvku/prvcích
  3. start(interval = 100)
    - sputí smyčku sledování a kontroluje stav v pravidelných intervalech 9pokud není zadáno tak po 100 milisekundách)
  4. stop()
    - ukončí smyčku sledování
Celá třída pak vypadá následovně:

var VisibilityChangedObserver = function () {
    var registrations = [];
    var timerId = null;
    var self = this;

    var checkVisiblities = function() {
        "use strict";
        for (var i = 0; i < registrations.length; i++) {
            var registration = registrations[i];
            var wasVisible = $(registration).data("visibilityChangedPrevious") == "visible";
            var isVisible = $(registration).is(":visible");

            if (wasVisible != isVisible) {
                $(registration).data("visibilityChangedPrevious", isVisible ? "visible" : "hidden");
                $(registration).trigger("visibilityChanged");
            }
        }
    };

    this.register = function($selector, callback) {
        "use strict";
        $selector.each(function() {
            registrations.push(this);
            $(this).on("visibilityChanged", function(event) {
                event.result = $(this).is(":visible");
                callback(event);
            });
        });
        return this;
    };

    this.deregister = function($selector) {
        "use strict";
        var index = registrations.indexOf($selector);
        if (index > -1) {
            registrations = registrations.splice(index, 1);
        }
    };

    this.start = function() {
        "use strict";
        timerId = window.setInterval(checkVisiblities, 100);
        return this;
    };

    this.stop = function() {
        "use strict";
        if (timerId != null) {
            window.clearInterval(timerId);
        }
        return this;
    };
};

V následujícím příkladu si můžete uvedenou funkcionalitu vyzkoušet. Spustitelné demo na http://jsfiddle.net/8gPq6/. Čtyři tlačítka v headeru zobrazují/skrývají odpovídající elementy, do patičky pak právě pomocí nové události přicházejí zprávy o viditelnosti.


<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Sample</title>
<link rel="stylesheet" type="text/css" href="site.css" />
<script type="text/javascript" src="jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="visibilityChangedObserver.js"></script>
<script type="text/javascript">

var messageStack = [];

var updateStatusMessage = function(message){
 messageStack.push(message);
 while (messageStack.length > 3) {
  messageStack.shift();
 }
 $("footer").html("<p>" + messageStack.join("<br />") + "</p>");
};

$(document).ready(
 function(){
  $("button").on("click", function(event){
   var asociated = $(this).data("asociated");
   $(asociated).toggle();
  });

  var observer = new VisibilityChangedObserver()
   .register($(".visibilityCheck"), function(event){
    var message = "Element with #id = '" + $(event.target).attr("id") + "' is " + ((event.result) ? "visible." : "hidden.");
    updateStatusMessage(message);
   })
   .start();
 }
);


</script>
</head>
<body>

<header>
 <button id="btnOuter" data-asociated="#outer">Toggle outer</button>
 <button id="btnInner1" data-asociated="#inner1">Toggle inner1</button>
 <button id="btnInner2" data-asociated="#inner2">Toggle inner2</button>
 <button id="btnInner3" data-asociated="#inner3">Toggle inner3</button>
</header>

<div id="main">

 <div id="outer">
  <p>Outer DIV</p>
  <div id="inner1" class="inner visibilityCheck">
   <p>Inner DIV1</p>
  </div>
  <div id="inner2" class="inner visibilityCheck">
   <p>Inner DIV2</p>
  </div>
  <div id="inner3" class="inner visibilityCheck">
   <p>Inner DIV3</p>
  </div>
 </div>

</div>

<footer>
</footer>
</body>
</html>

středa 6. listopadu 2013

Defaultní hodnoty snadno a rychle

Pokud potřebujeme defaultní hodnotu nějakého typu a tento typ předem známe, je to vcelku jednoduchá záležitost - stačí zavolat metodu default(T) kde T je požadovaný typ.

Pokud daný typ dopředu neznáme (je nám předaný napřííklad jako parametr metody), musíme si pomoci jinak - ke slovu přichází reflexe. K tomuto účelu používám statickou helper třídu TypeExtensionsMethods, která jak název napovídá obsahuje extension metody pro instance typu Type a přidává třídě Type metodu GetDefaultValue().

Pro typy, které nejsou hodnotové je výsledek jednoduchý - null, u hodnotových typů si pomůžeme zavoláním generické metody GetDefaultGenericValue() pomoci reflexe. A vzhledem k tomu, že reflexe má dopady na výkon, je dobré si takovéto hodnoty kešovat pro budoucí volání. K tomuto účelu se hodí ukládání do ConcurrentDictionary, který je thread-safe, takže nemusíme řešit zámky v případě vícevláknového volání.

Pozn.: Metoda GetDefaultGenericValue() by mohla být private, protože její volání nikde jinde nepoužívám (ani nemá význam vzhledem k existenci systémového default(T)). Jelikož mám nainstalovaný doplněk Resharper ve Visual Studiu, který nevidí použití metody přes reflexi a přesvědčuje mě, že je dobrý nápad tuto nevyužitou metodu smazat, tak jsem ji označil pro jednoduchost jako public.


using System.Collections.Concurrent;
using System.Reflection;

namespace System
{
    public static class TypeExtensionsMethods
    {
        private static readonly ConcurrentDictionary<Type, object> TypeDefaults = new ConcurrentDictionary<Type, object>();

        public static T GetDefaultGenericValue<T>()
        {
            return default(T);
        }

        public static object GetDefaultValue(this Type type)
        {
            if (type == null) throw new ArgumentNullException("type");

            return type.IsValueType
                ? TypeDefaults.GetOrAdd(type, t => typeof(TypeExtensionsMethods).GetMethod("GetDefaultGenericValue", BindingFlags.Static | BindingFlags.Public).MakeGenericMethod(t).Invoke(null, null))
                : null;
        }

    }
}

Pokud někde v kódu potřebuju získat defaultní hodnotu pro typ předaný prametrem, stačí postupovat takto.

private void Foo(Type type) 
{
    var defaultValue = type.GetDefaultValue();
    Console.WriteLine(defaultValue);
}

Klasik by zvolal: "Jak snadné, milý Watsone!" :-)

pondělí 14. října 2013

Vícesloupcové kombo - čistě v XAMLu

Na WPFku se mi líbí to, že ačkoliv obsahuje poměrně jednouchou sadu kontrolů ve svém základu, tak díky úpravám templatů je možné si jednotlivé stavební prvky upravovat téměř libovolně a ve výsledku žádně dvě aplikace nemusí vypadat stejně.

Tentokrát si upravíme kombo tak, aby po otevření zobrazovalo více sloupců než jen jeden a uživatel tak měl snazší výběr hodnoty.
vícesloupcové kombo

Dalo by se to řešit přepsáním ControlTemplate, a nahradit stávající ItemsPresenter za nějakou jinou kontrolku podporující zobrazení kolekcí po sloupcích. Jakýkoliv zásah do ControlTemplate je ale poměrně pracný, navíc je potřeba přepsat všechny varianty (Classic, Aero, Luna, ...) aby nový kontrol nepůsobil jak pěst na oko v jiném tématu.

Svoje řešení jsem tedy postavil na změně DataTemplate, kdy si připravím dvakrát ItemTemplate - jeden pro zavřené kombo a jeden pro otevřené (prvky v Popup okně) a ty nastavuji pomocí triggeru ve stylu pro dané kombo. Sloupce v jednotlivých řádcích jsou zarovnané díky nastavenému Grid.IsSharedSizeScope v rodičovském kombu a pojmenovaným skupinám sloupečků v ItemTemplate (SharedSizeGroup). Drobná úprava stylu pro ComboBoxItem v rámci komba pak vyhazuje přebytečný padding a rámeček vybraného záznamu, které vizuelně rušily díky vlastnímu paddingu a rámečkům v jednotlivých buňkách.

Výběr správného ItemTemplate zajišťuje trigger, jak už je uvedeno výše a fígl jak vybrat ten správný spočívá v tom, že v otevřeném kombu uvnitř Popup okna jsou jednotlivé prvky uzavřené v kontejneru ComboBoxItem, zatímco tzv. SelectionBox tento kontejner nad sebou nemá. DataTrigger tedy zkouší mezi svými předky najít kontrol ComboBoxItem a pokud ho nenajde (výsledek je null) použije DataTemplate pro zavřené kombo, v opačném případě pro otevřené.

V ukázce používám jednoduchý ViewModel s kolekcí lidí (IList), z této je možné vybírat pomocí komba kde v zavřené podobě na GUI vidíme celé jméno a v otevřené jsou vidět veškeré informace o člověku (jméno, příjmení, pohlaví a věk).

Třída Person a ukázkový viewmodel vypadá následovně:

public class Person
{
    public Person(string name, string surname, bool male, DateTime birthDate)
    {
        Name = name;
        Surname = surname;
        Male = male;
        Age = (int)(DateTime.Now - birthDate).TotalDays / 365;
    }

    public string Name { get; private set; }
    public string Surname { get; private set; }
    public bool Male { get; private set; }
    public int Age{ get; private set; }
    public string FullName { get { return string.Format("{0} {1}", Surname, Name); } }
}

public class MainViewModel
{
    public MainViewModel()
    {
        People = new ObservableCollection<Person>
        {
            new Person("Zdeněk", "Srstka", true, new DateTime(1935, 9, 26)),
            new Person("Dana", "Morávková", false, new DateTime(1971, 7, 29)),
            new Person("Jiří", "Pomeje", true, new DateTime(1964, 12, 13))
        };
        CurrentPerson = People[0];
    }

    public IList<Person> People { get; private set; }
    public Person CurrentPerson { get; set; }
}

XAML view pak takto:

<Window x:Class="WpfApplication1.Views.MainView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"
        xmlns:converters="clr-namespace:WpfApplication1.Converters"
        mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:MainViewModel}"
        Title="MainView" Height="640" Width="480" >
    <Window.Resources>

        <!-- konverter převádějící bool hodnotu na uvedený řetězcový zápis -->
        <converters:BooleanToObjectConverter x:Key="BooleanToObjectConverter" TrueValue="♂"  FalseValue="♀" />

        <!-- DataTemplate pro zavřené kombo (SelectionBox) -->
        <DataTemplate x:Key="SelectionBoxItemTemplate" DataType="viewModels:Person">
            <TextBlock Text="{Binding Path=FullName}" />
        </DataTemplate>

        <!-- DataTemplate prvky uvnitř popupu (4 sloupečky) -->
        <DataTemplate x:Key="PopupItemTemplate" DataType="viewModels:Person">
            <Grid>
                <Grid.Resources>
                    <Style TargetType="{x:Type Border}">
                        <Setter Property="BorderBrush" Value="Silver" />
                        <Setter Property="BorderThickness" Value="0.25" />
                        <Setter Property="Padding" Value="5, 2" />
                    </Style>
                </Grid.Resources>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition SharedSizeGroup="A" />
                    <ColumnDefinition SharedSizeGroup="B" />
                    <ColumnDefinition SharedSizeGroup="C" />
                    <ColumnDefinition SharedSizeGroup="D" />
                </Grid.ColumnDefinitions>
                <Border Grid.Column="0">
                    <TextBlock Text="{Binding Path=Surname}" />
                </Border>
                <Border Grid.Column="1">
                    <TextBlock Text="{Binding Path=Name}" />
                </Border>
                <Border Grid.Column="2">
                    <TextBlock Text="{Binding Path=Male, Converter={StaticResource BooleanToObjectConverter}}" />
                </Border>
                <Border Grid.Column="3">
                    <TextBlock Text="{Binding Path=Age, StringFormat={}{0} let}" />
                </Border>
            </Grid>
        </DataTemplate>
        
        <!-- styl pro upravené kombo -->
        <Style x:Key="PeopleComboBoxStyle" TargetType="{x:Type ComboBox}">
            <Style.Resources>
                <Style TargetType="{x:Type ComboBoxItem}">
                    <Setter Property="Padding" Value="0" />
                    <Setter Property="BorderThickness" Value="0" />
                </Style>
            </Style.Resources>
            <Setter Property="Grid.IsSharedSizeScope" Value="True" />
            <Setter Property="ItemTemplate">
                <Setter.Value>
                    <DataTemplate>
                        <ContentControl x:Name="cnt" Content="{Binding}" ContentTemplate="{StaticResource PopupItemTemplate}" />
                        <DataTemplate.Triggers>
                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}}" Value="{x:Null}">
                                <Setter TargetName="cnt" Property="ContentTemplate" Value="{StaticResource SelectionBoxItemTemplate}" />
                            </DataTrigger>
                        </DataTemplate.Triggers>
                    </DataTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    </Window.Resources>
    <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
        
        <ComboBox ItemsSource="{Binding Path=People}"
                  SelectedItem="{Binding Path=CurrentPerson}"
                  Style="{StaticResource PeopleComboBoxStyle}" />
        
    </Grid>
</Window>