Miłosz Orzeł

.net, js, html, arduino, java... no rants or clickbaits.

Jak zamknąć okna pop-up przy zamknięciu okna głównego lub wylogowaniu?

Wyobraź sobie, że dostałeś zlecenie serwisowe dla pewnej bardzo starej aplikacji webowej. Aplikacja ma jedno okno główne i okna pop-up, w których prezentowane są poufne informacje (np. dane płacowe). Klient chciałby by wszystkie pop-upy zostały automatycznie zamknięte gdy użytkownik opuści główne okno aplikacji lub naciśnie w tym oknie przycisk „wyloguj”… 

Więc… jak zamknąć wszystkie okna otworzone za pomocą metody window.open?

W sieci pytanie to pada bardzo często. Niestety najczęściej podawane jest naiwne rozwiązanie problemu, które polega na zapamiętaniu referencji do otwartych okien i późniejsze wywołanie metody close na pop-upach:

var popups = []; 

function openPopup() {
    var wnd = window.open('Home/Popup', 'popup' + popups.length, 'height=300,width=300');
    
    popups.push(wnd);
}

function closePopups() {
    for (var i = 0; i < popups.length; i++) {
        popups[i].close();
    }

    popups = [];
}

W praktyce to nie zadziała bo przecież tablica z referencjami zostaje wyczyszczona przy pełnym przeładowaniu strony (np. po kliknięciu w link albo przy postbacku)…

Inne sugerowane rozwiązanie to nadanie pop-upowi unikalnej nazwy (za pomocą drugiego parametru metody open) i późniejsze pozyskanie referencji do okna:

var wnd = window.open('', 'popup0');
wnd.close();

Chodzi o skorzystanie z faktu, że metoda window.open działa na 2 sposoby:

  1. Jeśli okno o zadanej nazwie nie istnieje, zostanie utworzone.
  2. Jeśli okno o zadanej nazwie istnieje, nie zostanie ponownie utworzone, zamiast tego zostanie zwrócona referencja do niego (jeśli zostanie podany niepusty URL zawartość okna zostanie dodatkowo przeładowana).

Problem leży w punkcie nr 1. Jeśli okno pop-up o danej nazwie nie było wcześniej otwarte, zawołanie open a potem close, spowoduje, że przez krótką chwilę pop-up będzie widoczny. Lipa…

A może da się przechować referencję do pop-upa między przeładowaniami strony?

Jeśli nie istnieje konieczność obsługi starych przeglądarek (mało prawdopodobne dla starej aplikacji) można by spróbować zapisać wskazanie na pop-up do localStorage. To jednak się nie uda:

var popup = window.open('http://morzel.net', 'test');
localStorage.setItem('key', JSON.stringify(popup)); 

TypeError: Converting circular structure to JSON

Stare tricki z utrzymaniem stanu strony oparte o cookies lub window.name też nie zadziałają.

 

Wiec… co z tym zrobić?

Nawet jeśli nie możesz sobie pozwolić na dużą zmianę jaką jest wprowadzenie ramek nie poddawaj się :)

Okna popup mają właściwość opener, która wskazuje na okno nadrzędne (tzn. to w którym znajdowało się wywołanie window.open). Okna pop-up mogą więc cyklicznie sprawdzać czy otwierające je okno główne jest nadal otwarte. Mogą też odwoływać się do zmiennych w oknie nadrzędnym. Fakt ten można wykorzystać do wymuszenia zamknięcia okien pop-up gdy użytkownik naciśnie przycisk „wyloguj” w oknie głównym. Gdy użytkownik jest zalogowany (i tylko wtedy!), należy dodać do obiektu window okna głównego znacznik zalogowania w postaci zmiennej (np. loggedIn).

Oto kod JS, który powinien zostać umieszczony na stronie wyświetlanej w pup-upie:

window.setInterval(function () {
    try {
        if (!window.opener || window.opener.closed === true || window.opener.loggedIn !== true) {
            window.close();
        }
    } catch (ex) {
        window.close(); // FF może rzucić wyjątek bezpieczeństwa przy próbie odwołania do loggedIn (dla zewnętrznego serwisu)
    }
}, 1000);

Sprawdzanie zmiennej z okna opener ma jeszcze jedną zaletę. Jeśli użytkownik w oknie głównym przejdzie do innej aplikacji (np. poprzez przycisk wstecz lub link do zewnętrznego serwisu) wówczas okna pop-up wykryją brak monitorowanej właściwości window.opener i zamkną się automatycznie.

Cóż, nie jest to kod który lubisz pisać ale osiąga pożądany efekt nawet pomimo bolesnych braków w API przeglądarek. Gdyby tylko zrobili metodę window.exists('name')…  

Modyfikator ref dla typów referencyjnych i odrobina SOSu

Spójrz na poniższy kod i zastanów się jaka wartość zostanie wyświetlona na konsoli (pamiętaj, że string to typ referencyjny)?

using System;
               
class Program
{
    static void Test(string y)
    {
        y = "bbb";
    }

    static void Main()
    {
        string x = "aaa";
        Test(x);
        Console.WriteLine(x);
    }
}

Prawidłowa odpowiedź (aaa) nie jest wcale taka oczywista. Użytkownik zobaczy napis aaa, dlatego, że bez użycia modyfikatora ref, program napisany w C# przekazuje do metody kopię wartości parametru (dla typów wartościowych) lub kopię referencji (dla typów referencyjnych).

Gdy do parametru y w metodzie Test przypisywany jest nowy tekst, CLR nie modyfkuje tablicy znaków. Zamiast tego tworzy nowy string (więcej info tutaj) i przypisuje wskazanie do niego do zmiennej y. Zmienna y znajdująca się w metodzie Test jest jednak tylko kopią referencji do napisu wskazywanego przez zmienną x z metody Main. Skoro zmodyfikowana została jedynie kopia, to po wyjściu z metody, na konsole trafia pierwotny napis aaa.

By rzeczywiście zmienić tekst kryjący się pod zmienną x, użyj modyfikatora ref (musisz dodać go zarówno w deklaracji metody jak i jej wywołaniu – C# wymusza takie zachowanie by zwiększyć czytelność kodu):

using System;
               
class Program
{
    static void Test(ref string y)
    {
        y = "bbb";
    }

    static void Main()
    {
        string x = "aaa";
        Test(ref x);
        Console.WriteLine(x);
    }
}

Po takiej zmianie na konsole trafi napis bbb.

 

SOS

Sposób przekazywania parametrów do metody można zbadać za pomocą narzędzia SOS (Son of Strike). Posłużymy się poleceniem CLRStack -a, które wyświetli informacje o parametrach i zmiennych lokalnych na stosie kodu zarządzanego (jeśli nie wiesz jak używać SOS patrz tutaj i tutaj, jeśli dziwisz się skąd nazwa "Son of Strike" kliknij tu)...

Poniżej znajdują się rezultaty polecania CLRStack -a, wykonanego w momencie wejścia do metody Test.

Dla kodu bez modyfikatora ref:

!CLRStack -a
OS Thread Id: 0x176c (5996)
Child SP IP       Call Site
0031f114 00390104 Program.Test(System.String)
    PARAMETERS:
        y (0x0031f114) = 0x025cb948

0031f158 003900af Program.Main()
    LOCALS:
        0x0031f158 = 0x025cb948

0031f3c0 656721bb [GCFrame: 0031f3c0]

Dla kodu z modyfikatorem ref:

!CLRStack -a
OS Thread Id: 0x934 (2356)
Child SP IP       Call Site
001dee34 002f00f4 Program.Test(System.String ByRef)
    PARAMETERS:
        y (0x001dee34) = 0x001dee78

001dee78 002f00aa Program.Main()
    LOCALS:
        0x001dee78 = 0x027fb948

001df0ec 656721bb [GCFrame: 001df0ec]

Istotną różnicą widoczną na powyższych zrzutach jest wartość parametru y. W przypadku kodu bez modyfikatora ref jest to adres stringa aaa (0x025cb948), natomiast dla kodu z modyfikatorem ref, wartością parametru y jest adres zmiennej x z metody Main (0x001dee78)która wskazuje na string aaa.

View State dla TextBox i innych kontrolek implementujących IPostBackDataHandler

Czytając oficjalny trening kit do egzaminu 70-515 natrafiłem na taki tekst: "With view state, data is stored within controls on a page. For example, if a user types an address into a TextBox and view state is enabled, the address will remain in the TextBox between requests.". Skoro takie zdania padają w zalecanym podręczniku, nic dziwnego, że łatwo pogubić się w tym jak ASP.NET Web Forms próbuje radzić sobie z naturalną dla HTTP bezstanowością... ;)

Kontrolka TextBox ze strony ASPX:

<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>

Jest na stronie HTML renderowana jako tag input:

<input name="TextBox1" type="text" id="TextBox1" />

Skoro tak, to dla zachowania wartości kontrolki między requestami nie ma konieczności przechowywania wartości pola TextBox1 w ukrytym polu __VIEWSTATE. By się o tym przekonać stwórz prostą stronę zawierajacą kontrolki TextBox i Button:

...

<body>
    <form id="form1" runat="server">
    	<asp:TextBox ID="TextBox1" runat="server"></asp:TextBox>
    	<asp:Button ID="Button1" runat="server" Text="Button" onclick="Button1_Click" /
    </form>
</body>
</html>

i dodaj handler dla zdarzenia Click przycisku, którego jedynym zadaniem jest rozwijanie tekstu znajdującego się w kontolce TextBox1:

protected void Button1_Click(object sender, EventArgs e)
{
    TextBox1.Text += "X";
}

Następnie uruchom stronę i aktywuj narzędzie pozwalające na monitorowania komunikacji pomiędzy przeglądarką a serwerem. Interesuje nas badanie danych formularza przesyłanych na serwer przy postbacku... Jeśli używasz IE polecam program Fiddler, pod Firefoxem skorzystaj z dodatku Firebug. Możesz też użyć wbudowanego w ASP.NET mechanizmu Trace - w tym celu dopisz Trace="true" do dyrektywy @Page. Ja przeprowadzę test przy użyciu narzędzi deweloperskich dostarczonych wraz z przeglądarką Chrome (zakładka "Network").

Na poniższym screenie widać jakie dane formularza (request HTTP POST) zostały wysłane przy pierwszym naciśnięciu przycisku:

Dane formularza przy pierwszym postbacku

Tutaj widać dane z drugiego postbacka:

Dane formularza przy drugim postbacku

Jeśli porównasz dane z pierwszego i drugiego żądania, zobaczysz, że zmiana wartości TextBox1.Text nie wpływa na wartość pola __VIEWSTATE. Rozszerzanie tego pola byłoby marnotrastwem zasobów łącza skoro tekst jest wysyłany na serwer w osobnym polu o nazwie TextBox1.

Klasa System.Web.UI.WebControls.TextBox jest jedną z kilku klas implementującyh interfejs IPostBackDataHandler. Interfejs ten wymaga istnienia metody LoadPostData. Po zakończeniu incjalizacji strony (ale przed zdarzeniem Load), wykonywane jest ładowanie danych z View State (LoadViewState) a następnie (o ile kontrolka implementuje IPostBackDataHandler) z danych formulrza (LoadPostData). Właściwość Text kontolki typu TextBox może być zatem ustawiona nawet jeśli mechanizm View State jest wyłączony (poprzez ustawienie EnableViewState="false").

Czy nie można więc zupełnie zrezygnować z mechanizmu View State dla TextBoxa i podobnych kontrolek?

Nie. View State przydaje się np. wtedy gdy obsługiwane jest zdarzenie TextChanged (dla porównania wartości aktualnej i poprzedniej). Może mieć też zastosowanie gdy ustawiana jest inna właściwość kontrolki niż ta utożsamiana z wartością pola (np. ForeColor).

Wykrycie załadowania iframe stworzonego w Ext JS

Przypuśćmy, że zachodzi potrzeba wykonania jakiegoś fragmentu kodu w momencie załadowania zawartości iframe. W przy- padku gdy iframe jest utworzony statycznie w kodzie HTML strony, sprawa jest bardzo prosta. Wystarczy podłączyć funkcję JavaScript pod zdarzenie load:

<iframe src="http://wikipedia.org" width="600" height="400" onload="someFunction();" ></iframe>

Uwaga: zdarzenie load (onload) zostanie wywołane gdy cała zawartość dokumentu zostanie załadowana (w tym jego elementy zewnętrzne takie jak obrazki). Jeśli trzeba zadziałać wcześniej, tzn. w chwili gdy DOM jest gotowy, należy skorzystać z innych metod...

A co w przypadku iframe’a stworzonego w kodzie Ext JS?

Prostym sposobem na utworzenie go jest użycie Ext.BoxComponent z odpowiednio ustawioną wartością autoEl. Daje to możliwość łatwego wpisania iframe’a w layout Ext JS (np. jako składowej Ext.Window) bez rozszerzania drzewa dokumentu o zbędne elementy.

var iframeContainer = new Ext.BoxComponent({
    autoEl: {
        tag: 'iframe',
        frameborder: '0',
        src: 'http://wikipedia.org'
    },
    listeners: {
        afterrender: function () {
            console.log('rendered');

            this.getEl().on('load', function () {
                console.log('loaded');
            });
        }
    }
});


W powyższym kodzie (Ext JS 3.2.1) bardzo istotny jest moment, w którym pod zdarzenie load iframe’a podpinany jest handler. Można to zrobić jedynie po tym gdy kontrolka (BoxComponent) zostanie wyrenderowana. Jeśli zrobi się to wcześniej, wówczas wywołanie getEl() zwróci undefined i kod nie zadziała. Przed renderowaniem kontrolka Ext JS to jedynie obiekt JavaScript, dla którego nie istnieją żadne elementy w drzewie dokumentu. Poniżej znajdują się dwa screeny prezentujące fragmenty kodu HTML utworzonego przez Ext.Window, w którym jedynym elementem na liście items był BoxComponent tworzący tag iframe...

beforerender:

DOM beforerender

afterrender:

DOM afterrender


Widać wyraźnie, że zbyt wczesne podpinanie się pod load nie ma prawa zadziałać bo nie można przecież nasłuchiwać zdarzeń na czymś co nie isntnieje.

Powyższe zrzuty pochodzą z okna Elements narzędzia Chrome Developers Tools. Szybkim sposobem by pokazać to narzędzie (oczywiście w przeglądarce Googla) jest naciśnięcie F12 lub Ctrl+Shift+I. Fajną funkcją CDT jest możliwość wylistowania zdarzeń nasłuchiwanych na określonym elemencie DOM. By zobaczyć listę trzeba zaznaczyć element DOM i w menu po prawej stronie wybrać zakładkę Event Listeners. Na screenie poniżej widać, że iframe faktycznie posiada obsługę zdarzenia load:

CDT Event Listeners

BadImageFormatException, x86 i x64

Czy natrafiłeś kiedyś na wyjątek typu BadImageFormatException lub komunikat "An attempt was made to load a program with an incorrect format" ("Próbowano załadować program w niepoprawnym formacie")?

Jeśli tak to być może program, który próbowałeś uruchomić nie został skompilowany z użyciem opcji /platform:x86. Zastanawiasz się pewnie dlaczego podczas programowania w C# powinieneś przejmować się tym na jakiej platformie (x86/x64) kod zostanie wykonany. Cóż, w większości przypadków nie musisz o tym myśleć. Jeśli nie używasz bloków unsafe ani nie importujesz natywnych modułów problemu nie ma, bowiem Twój kod C# zostaje przetłumaczony do kodu pośredniego (CIL), który przed wykonaniem zostaje skompilowany (JIT) na postać odpowiednią dla platformy docelowej. Ok, ale...

Wyobraź sobie, że używasz w swojej aplikacji funkcji importowanej z 32 bitowej DLL. Gdy uruchomisz program na 32 bitowym systemie wszystko działa jak należy. Niestety na maszynie x64 otrzymujesz wspominany wyjątek BadImageFormatException. Dlaczego? Jeśli assembly importujące DLL zostało skompilowane z opcją /platform:anycpu to do jego uruchomienia na systemie 64 bitowym została użyta 64 bitowa wersja CLR. Próba załadowania 32 bitowej DLL z aplikacji działającej w procesie 64 bitowym nie powiedzie się. Gdyby do kompilacji została użyta opcja /platform:x86 wówczas na 64 bitowym systemie operacyjnym program zostałby uruchomiony w 32 bitowej wersji CLR (z użyciem WoW64: Windows 32-bit on Windows 64-bit).

Platformę docelową (przełącznik /platform kompilatora C#) można w Visual Studio 2010 ustawić w oknie “Properties” na zakładce “Build”. By tam trafić kliknij prawym klawiszem w plik projektu w Solution Explorer i wybierz „Properties” lub użyj menu głównego „Project | <nazwa projektu> Properties…”.

Ustawienie platformy docelowej w Visual Studio 2010. Kliknij aby powiększyć...

Microsoft stworzył przydatne narzędzie wiersza poleceń o nazwie CorFlags, które służy między innymi do podglądu lub ustawiania platformy docelowej. Dostęp do tego narzędzia można uzyskać korzystając z Visual Studio Command Prompt albo przez znalezienie go bezpośrednio na dysku (u mnie jesto ono pod C:\Program Files\Microsoft.NET\SDK\v2.0\Bin\CorFlags.exe)

Poniżej znajduje się kilka przykładów tego co możesz zobaczyć po sprawdzeniu plików EXE stworzonych z różnymi wartościami opcji /platform kompilatora (do sprawdzenia pliku służy polecenie: CorFlags nazwa.pliku):

anycpu x86 x64
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32
CorFlags  : 3
ILONLY    : 1
32BIT     : 1
Signed    : 0
Version   : v4.0.30319
CLR Header: 2.5
PE        : PE32+
CorFlags  : 1
ILONLY    : 1
32BIT     : 0
Signed    : 0

W kontekście tego posta istotne są 2 rzędy z wyniku zwróconego przez CorFlags: PE i 32BIT.

  • PE: PE32 oznacza, że plik może być wykonany w środowisku x86 i x64
  • PE: PE32+ oznacza, że plik może być wykonany jedynie w środowisku 64 bitowym
  • 32BIT: 1 oznacza, że program musi być wykonany w środowisku x86

Zrozumienie znaczenia 32BIT: 1 jest naprawdę istotne jeśli chcesz uniknąć problemów z importowanie 32 bitowch DLL na 64 bitowej wersji Windows. Jeśli flaga 32BIT jest ustawiona i uruchomisz plik PE32 na x64 wówczas Twoja aplikacja zostanie uruchomiona w środowisku 32 bitowym (z użyciem WoW), dzięki czemu zaistnieje możliwość zaimportowania 32 bitowej DLL. Jeśli flaga 32BIT nie jest ustawiona, aplikacja uruchomi się w procesie 64 bitowym – co spowoduje problem z załadowaniem biblioteki.

Dzięki CorFlags można w łatwy sposób zmodyfikować wartość flagi 32BIT. Do jej ustawienia służy przełącznik /32BIT+

CorFlags file.exe /32BIT+

A do jej usuwania /32BIT-

CorFlags file.exe /32BIT-

Tak więc nawet jeśli nie masz możliwości przekompilowania problematycznego kodu z odpowiednią opcją /platform nadal możesz użyć 32 bitowej DLL w 64 bitowej wersji systemu Windows :)