\


 Monday, 12 January 2009
Odhlášení uživatele z aplikace po uplynutí nastavené doby

Jedním z požadavků na Pocket PC (Windows Mobile ) aplikace je  odhlášení uživatele po uplynutí stanovené doby, kdy s aplikací nepracoval, aby se zmenšilo riziko, že uživatel někde PDA položí, někdo jiný PDA  najde a ochutná z gurmánského menu  položku "co nám dnes servírují za citlivé údaje" nebo  se vrhne na žertovné a bezrizikové mazání dat na serveru pod identitou nenáviděného kolegy. Tedy alespoň nějak takto si představuji důvody, kvůli kterým zákazníci na automatickém odhlašování tak lpějí. :)

Jestliže jste někdy zkusili automatické odhlašování napsat, víte, že kvůli podivnému chování modálních dialogů ve Windows Mobile se nejedná o příjemný úkol. Mezi podivné chování modálních dialogu počítám to, že ve správci procesů vždy vidíte všechny  formuláře, ne pouze poslední otevřený, a navíc např. třída v Symbol SDK pro práci se čtečkou čárových kódů při zobrazení více modálních formulářů tiše a nehrdinsky zhyne.

Podívejme se na jedno z možných řešení.

Všechny formuláře v aplikaci budou skryté za rozhraním IStackForm.

    public interface IStackForm
    {
        void BeginShowErrors();
        void EndShowErrors();        
        void CloseForm();
        void NotifyInactivityTimeout();
        void SetLastTitle();
        void ResetTitle();        
    }

Metoda BeginShowErrors notifikuje formulář, že je v aplikaci aktivním formulářem, který má zobrazovat případné uživatelské chyby. Metoda EndShowErrors sděluje formuláři, že nyní nemá na nastalé chyby sám reagovat. Jak uvidíme, metoda EndShowErrors se nám bude hodit proto, abychom potlačili chybová hlášení při odhlašování uživatele a místo odhlášení tak nezůstali v nějakém, nyní pro uživatele zcela nezajímavém, dialogu.

Metoda CloseForm uzavře formulář. Metoda NotifyInactivityTimeout slouží k informování formuláře, že uživatel bude odhlášen z aplikace, a formulář může uložit změny nebo uvolnit používané prostředky .

Metoda Reset Title přikazuje formuláři, aby nastavil svůj titulek (vlastnost Text) na prázný řetězec. Vlastnost SetlastTitle nutí formulář, aby obnovil svůj titulek na hodnotu, kterou měl  před voláním metody ResetTitle. Tyto na první (a možná i na druhý) pohled podivné kejkle slouží k simulaci modálního formuláře - přesněji řečeno, využívám toho, že ve správci úloh se zobrazí pouze formuláře s nastaveným titulkem. Jestliže máte v aplikaci pouze jeden formulář s neprázdným titulkem, tak jen tento formulář je zobrazen ve správci úloh a další formuláře s prázdným titulkem nejsou viditelné a ani běžným způsobem pro uživatele dostupné.

 

Takto může vypadat bázová třída FormBase (Layer Supertype pro UI) s výchozí implementací rozhraní IStackForm.

    public class FormBase : Form, IStackForm
    {
        [DllImport("coredll", EntryPoint = "SetForegroundWindow")]
        private static extern bool SetForegroundWindow(IntPtr hWnd); 

        #region protected variables
        protected bool m_InactivityTimeout;
        protected bool m_CloseRequired;
        protected string m_LastTitle;
        protected FormNativeWrapper m_NativeWrapper;
        protected bool m_InError;
        #endregion protected  variables
        #region IStackForm Members
        public  delegate void MyInvokeDelegate();
        public FormBase()
        {
            InitializeComponent();
            m_LastTitle = String.Empty;
            KeyPreview = true;
            
            m_NativeWrapper = new FormNativeWrapper((Form)this);
        }

        private static void updateLastUpdate()
        {
            FormNativeWrapper.LastFormUpdate = DateTime.Now;
        }

        public virtual void BeginShowErrors()
        {
            ErrorInfo.NewError += new EventHandler<ErrorInfoEventArgs>(ErrorInfo_NewError);
        }

        void ErrorInfo_NewError(object sender, ErrorInfoEventArgs e)
        {
            if (InvokeRequired)
            {
                Invoke((EventHandler<ErrorInfoEventArgs>)ErrorInfo_NewError, sender, e);
                return;
            }

            m_InError = true;
            MessageBox.Show(e.Error.Description,
                             Resource1.General_AppCaption,
                             MessageBoxButtons.OK,
                             MessageBoxIcon.Exclamation,
                             MessageBoxDefaultButton.Button1);
        }

        public virtual void ClearErrorFlag()
        {
            m_InError = false;
            ErrorInfo.ClearErrors();
        }
        public virtual void EndShowErrors()
        {
            ErrorInfo.NewError -= new EventHandler<ErrorInfoEventArgs>(ErrorInfo_NewError);
        }

        public virtual void CloseForm()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)CloseForm);
                return;
            }

            Close();
        }

        public virtual void NotifyInactivityTimeout()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)NotifyInactivityTimeout);
                return;
            }
            
            m_InactivityTimeout = true;
            ResetTitle();

            try
            {
                if (User.CurrentUser != null)
                {

                    User.CurrentUser.Logout();
                    Session.Current.EndSession();
                }
            }
            catch (Exception e)
            {
                Debug.WriteLine(e);
            }
            
            //CloseForm();
        }

        public virtual void SetLastTitle()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)SetLastTitle);
                return;
            }
            
            Text = m_LastTitle;
            SetForegroundWindow(Handle);
        }

        public virtual void ResetTitle()
        {
            if (InvokeRequired)
            {
                Invoke((MyInvokeDelegate)ResetTitle);
                return;
            }
          m_LastTitle = Text;
          Text = String.Empty;
        }
        
        
         
        protected override void OnClosed(EventArgs e)
        {


            if (!m_InactivityTimeout)
            {
                FormStack.Instance.Pop();
            }

            base.OnClosed(e);
        }

        private void FormBase_Load(object sender, EventArgs e)
        {
            FormStack.Instance.Push(this);
            updateLastUpdate();
        }

    }

Kvůli souvislostem jsem v kódu ponechal i ukázku implementace metod BeginShowErrors a EndShowErrors. Již na úrovni FormBase zajistíme, že chyby jsou uživateli zobrazovány vždy tak, aby rodičem dialogu s chybovým hlášením byl aktuální formulář. Jak jsem už zmínil, metody SetLastTitle a ResetTitle pouze pracují s titulkem formuláře. U každé metody zajistíme, že je volána v UI threadu  - proto se na počátku metod objevuje vždy kontrola vlastnosti InvokeRequired a jestliže je metoda spuštěna v jiném threadu, metodou Invoke si vynutíme opětovné a nyní již bezpečné volání stejné metody v UI threadu.

Pro podporu automatického odhlášení uživatele jsou pro nás důležité tyté části kódu:

  1. Ihned v konstruktoru je každý formulář oředáh instanci třídy FormNativeWrapper.
    m_NativeWrapper = new FormNativeWrapper((Form)this);
    Třídu NativeWrapper uvidíme za chvíli, zde jen řeknu, že FormNativeWrapper sleduje vstupy od uživatele (stisknutí tlačítka, zadání textu...) a zajišťuje, že kdykoli uživatel pracuje s nějakým prvkem na formuláři, uloží se aktuální datum a  čas do proměnné, která nese informaci, kdy naposledy uživatel pracoval s aplikací. Chceme sledovat aktivity uživatele a proto si požádáme o zasílání všech stisknutých kláves (KeyPreview = true;).
  2. Při nahrání formuláře (FormBase_Load) je každý formulář vložen do zásobníku (LIFO kontajner) používaných formulářů.
    FormStack.Instance.Push(this);
    Kód třídy FormStack projdeme za chvíli. Současně také pomocí metody updateLastUpdate(); při nahrání formuláře uložíme informaci, že uživatel je aktivní. Metoda updateLastUpdate uloží aktuální datum a čas do statické vlastnosti FormNativeWrapper.LastFormUpdate.
  3. Při zavření formuláře (metoda OnClosed) vyjmeme formulář ze zásobníku, ale jen tehdy, jestliže je formulář zavřen uživatelem - pokud je formulář zavřen aplikací, protože je právě odhlašován uživatel, již se zásobníkem formulářů nepracujeme.
  4. V metodě NotifyInactivityTimeout, která je vyvolána vždy, když má být uživatel automaticky odhlášen, na úrovni předka zajistíme odhlášení uživatele voláním metody User.CurrentUser.Logout(); . V kódu jsem ponechal také řádky, které ukazují, že zde ukončíme například "session" uživatele a  uložíme změny.

 

Následuje třída FormNativeWrapper.

public class FormNativeWrapper : NativeWindow
    {
        private const int  WM_LBUTTONDOWN = 0x201;        
        private const int WM_KEYDOWN  = 0x0100;
        private const int WM_MOUSEMOVE = 0x0200;
        private const int WM_CHAR = 0x0102;
        private const int WM_COMMAND = 0x0111;


        private static DateTime m_lastUpdate;
        private static Object _lockObj = new object();

        public static DateTime LastFormUpdate
        {
            get
            {
                lock (_lockObj)
                {
                    return m_lastUpdate;
                }
            }
            set
            {
                lock(_lockObj)
                {
                    
                    m_lastUpdate = value;
                }
            }
        }
        public FormNativeWrapper(Form form)
        {            
            init(form);
        }

        public FormNativeWrapper(IStackForm form)
        {
            init(form as Form);
        }

        private void init(Form form)
        {
            if (form == null)
            {
                throw new ArgumentNullException("form");
            }

            if (form.Handle != IntPtr.Zero)
            {
                AssignHandle(form.Handle);
            }
            
            form.HandleCreated += ((sender, e) => AssignHandle(((Form)sender).Handle));
            form.HandleDestroyed += ((sender, e) => ReleaseHandle());
        }
        
        static FormNativeWrapper()
        {
            LastFormUpdate = DateTime.Now;
        }
        
        protected override void WndProc(ref Microsoft.WindowsCE.Forms.Message m)
        {
            if ((m.Msg == WM_KEYDOWN) ||
                (m.Msg == WM_LBUTTONDOWN)||
                (m.Msg == WM_MOUSEMOVE)||
                 (m.Msg == WM_CHAR) ||
                 (m.Msg == WM_COMMAND)
                )
            {
                LastFormUpdate = DateTime.Now;
            }
            base.WndProc(ref m);
        }
        
    }
  1. Datum poslední zaregistrované aktivity uživatele  - vlastnost LastFormUpdate  - máme pro jednoduchost přímo ve třídě FormNativeWrapper. Přistupovat k vlastnosti může kód běžící v různých threadech, a proto jsou přístupové metody obaleny sekcí lock.
  2. Třída FormNativeWrapper dědí z NativeWindow. Tato třída je součástí OpenNetCF frameworku. Třída zpracovává Windows zprávy zaslané formuláři, který jí byl předán do konstruktoru. V přepsané metodě WndProc nastavíme datum poslední aktivity uživatele, jestliže uživatel píše na klávesnici, používá stylus, stiskl tlačítko či vyvolal položku v menu.

S datem poslední aktivity musí pracovat další třída, která bude zodpovědná za informování dalších tříd v aplikaci, že již uživatel po stanovenou dobu s aplikací nepracoval a že by tedy měl být odhlášen. Takovou třídou je třída InactivitySimpleGuard.

 

    static class InactivitySimpleGuard
    {

        public static event EventHandler<EventArgs> UserInactivity;

        private const int INACTIVITY_CHECK = 10000;
        private static Timer _timer;
        private static int _inactivityInterval = Timeout.Infinite;
        

        public static void Start(int inactivityCheck)
        {
            if (_timer != null)
            {
                _timer.Dispose();
                _timer = null;
            }


            if (inactivityCheck != Timeout.Infinite)
            {
                _inactivityInterval = inactivityCheck;
                _timer = new Timer(onTimer, null, INACTIVITY_CHECK, INACTIVITY_CHECK);
            }
                                    
        }

        public static void Stop()
        {
            if (_timer != null)
            {
                _timer.Change(Timeout.Infinite, Timeout.Infinite);
                _timer.Dispose();
                _timer = null;
            }

        }

        private static void onTimer(object state)
        {
            if (User.CurrentUser != null &&
                checkUserInactivity())
            {
                OnUserInactivity(new EventArgs());
            }
                
        }

        private static void OnUserInactivity(EventArgs e)
        {
            if (UserInactivity != null)
            {
                UserInactivity(null, e);
            }
        }

        private static bool checkUserInactivity()
        {
            TimeSpan span = (DateTime.Now - FormNativeWrapper.LastFormUpdate);

            if (span.TotalSeconds > _inactivityInterval)
            {
                return true;            
            }
            
            return false;
        }
    }
  1. Třída InactivitySimpleGuard je statická, protože jsem měl své důvody v konkrétním řešení, proč ji udělat statickou. Nikdo vám samozřejmě nebrání udělat z třídy InactivitySimpleGuard běžnou instanční třídu, která bude singletonem, nebo budete mít (testovatelné) rozhraní, za něž bude dosazena konkrétní třída přes DI za běhu aplikace. V CNF je ale občas lepší ve jménu vyššího principu microsoftího zatratit své vzletné myšlenky. :)
  2. Rozhraní třídy je jednoduché. Metodě Start předáme počet sekund, které představují časový interval, po jehož  uplynutí je uživatel odhlášen, jestliže s aplikací nepracoval. Metoda Stop zruší objekt Timer, což znamená, že uživatel nebude nikdy automaticky odlogován, protože se jeho aktivita nebere v úvahu. Událost UserInactivity je vyvolána vždy, když uplyne zadaný časový interval, v němž uživatel s aplikací nepracoval, a aplikace tak může uživatele odhlásit. Kontrola doby neaktivity uživatele je v privátní metodě checkUserInactivity, která je volána metodou onTimer. Metoda onTimer je "callback" metodou, která je vyvolána objektem _timer vždy po uplynutí zadaného časového intervalu (délku časového intervalu intervalu  představuje konstanta INACTIVITY_CHECK).

Událost UserInactivity si přihlásí zásobník všech formulářu v aplikaci (třída FromStack) a po vyvolání události zavře všechny otevřené formuláře a zobrazí přihlašovací obrazovku. Následuje kód třídy FormStack.

class FormStack
    {
        private Stack<IStackForm> m_forms;
        private IStackForm prevForm;
        private bool m_noSetErrors;
        
        protected FormStack()
        {
            m_forms = new Stack<IStackForm>();
            InactivitySimpleGuard.UserInactivity += new EventHandler<EventArgs>(InactivitySimpleGuard_UserInactivity);
            m_noSetErrors = false;
        }

        void InactivitySimpleGuard_UserInactivity(object sender, EventArgs e)
        {
            try
            {
                InactivitySimpleGuard.Stop();
                //Konvence, že poslední formulář je vždy přihlašovací formulář
                m_noSetErrors = true;
                while (FormsCount() > 1)
                {
                    IStackForm frm = Pop();
                    frm.EndShowErrors();
                    frm.NotifyInactivityTimeout();
                    frm.CloseForm();
                }
                
                Debug.Assert(FormsCount() == 1);

                IStackForm loginFrm = Peek();

                try
                {
                    loginFrm.EndShowErrors();
                    loginFrm.SetLastTitle();
                    loginFrm.NotifyInactivityTimeout();
                    
                }
                finally
                {
                    loginFrm.BeginShowErrors();
                    m_noSetErrors = false;
                    //loginFrm.BeginShowErrors();
                }

            }
            
            catch (Exception ex)
            {
                //Ignorovat?
                Debug.WriteLine(ex);
            }
        }


        public virtual void Push(IStackForm form)
        {
            if (form == null)
            {
                throw new ArgumentNullException();
            }

            

            if (FormsCount() >= 1)
            {
                prevForm = m_forms.Peek();
                prevForm.ResetTitle();
                prevForm.EndShowErrors();
            }

            form.BeginShowErrors();
            m_forms.Push(form);
        }

        public virtual IStackForm Pop()
        {
            
            IStackForm retForm = m_forms.Pop();
            retForm.EndShowErrors();

            if (FormsCount() >= 1)
            {
                
                IStackForm currentForm = m_forms.Peek();
                if (!m_noSetErrors)
                {
                    currentForm.BeginShowErrors();
                }
                
                currentForm.SetLastTitle();
            }

            
            return retForm;
        }

        public virtual IStackForm Peek()
        {
            return m_forms.Peek();
        }
        
        
        public virtual int FormsCount()
        {
            return m_forms.Count;
        }
        
        
        public static FormStack Instance
        {
            get
            {
                return FormStackInternal.Stack;
            }
        }
            
        private class FormStackInternal
        {
            public static FormStack Stack = new FormStack();
        }
    }

Ve třídě FormStack jsou veškeré formuláře uloženy v pořadí, v němž byly zobrazeny. Za přidání a odebrání formulářů ze zásobníku je odpovědná třída FormBase, která používá veřejné metody Push a Pop.
Nás nyní hlavně zajímá obsluha události UserInactivity třídy InactivitySimpleGuard.

  1. V okamžiku, kdy je událost vyvolána, požádáme třídu InactivitySimpleGuard voláním její metody Stop, aby přestala aktivitu uživatele vyhodnocovat, protože uživatel má být odhlášen a je zbytečné a nesmyslné, aby byla událost UserInactivity vyvolávána, když není uživatel přihlášen.
  2. Postupně zavřeme všechny formuláře kromě přihlašovacího formuláře. Platí konvence, že poslední formulář v zásobníku, tedy první přidaný formulář, je přihlašovací formulář. Každý uzavíraný formulář nejprve dostane příkaz, aby nezobrazoval chybová hlášení, poté formuláři dovolíme zareagovat na vypršení doby neaktivity a nakonec formulář zavřeme.
    frm.EndShowErrors();
    frm.NotifyInactivityTimeout();
    frm.CloseForm();
  3. S přihlašovací formulářem zacházíme jinak. Také nechceme,aby prozatím zobrazoval chyby, dále u něj obnovíme titulek, aby byl viditelný i ve správci úloh jako aktivní formulář, a poté i jemu dáme šanci zareagovat na automatické odhlášení uživatele. Nakonec se přihlašovací formulář stane formulářem, který při novém přihlášení uživatele opět může zobrazovat nastalé chyby. Jak si můžete všimnout, v této fázi se snažíme nereagovat na výjimky -  naším hlavním cílem je zobrazení přihlašovacího formuláře.
    try
    {
    loginFrm.EndShowErrors();
    loginFrm.SetLastTitle();
    loginFrm.NotifyInactivityTimeout();
    }
    finally
    {
    loginFrm.BeginShowErrors();
    m_noSetErrors = false; //loginFrm.BeginShowErrors();
    }

 

Chceme-li odhlásit uživatele po uplynutí stanovené doby nečinnosti, stačí po startu aplikace zavolat metodu InactivitySimpleGuard.Start.

 

int timeOut = ServerConfiguration.Instance.GetPdaTimeout();
                        InactivitySimpleGuard.Start(timeOut);
 
 
 

Kód se dá dále vylepšit. FormNativeWrapper sleduje události na formuláři, ale když máte v aplikaci panel, tak některé notifikace pro rodiče okna pohltí panel a vlastnost LastFormUpdate není aktualizována. Potom by mohlo dojít k tomu, že uživatel je odhlášen, i když s aplikací pracuje. :-) Je ale jednoduché ve tříde  FormBase vyhledat všechny panely a obalit je nativním wrapperem. Vlastnost LastFormUpdate můžete také modifikovat při načtení libovolného čárového kódu - jinými slovy, sami jste zodpovědni za to, co bude aplikace považovat za aktivitu uživatele.



Monday, 12 January 2009 13:56:25 (Central Europe Standard Time, UTC+01:00)       
Comments [2]  Compact .Net Framework