niedziela, 27 marca 2011

Aplikacja z wykorzystaniem SqLite oraz WPF

Jak we wcześniejszym poście obiecałem, zamieszczę aplikację wykonaną przy użyciu bazy SqLite oraz technologi WPF o której już sporo się przewinęło na tym blogu.

Zadaniem było stworzenie programu umożliwiającego gromadzenie linków do stron, podzielonych na kategorie według uznania użytkownika.

Rozpoczynamy od schematu bazy danych:




Jak widać, 3 tabele:
Url - opisuje link
Category - kategoria danego linka
UrlCategory - tabela techniczna - umożliwia przypięcie linka do wielu kategorii

Interfejs programu zostanie napisany przy użyciu WPF.

Cały program wygląda następująco:

Wykorzystałem komponent Ribbon - wstążka znany z Microsoft Office. Użytkownik ma możliwość wyszukiwania linków po opisie.
Dodawanie kategorii:


Dodawanie linku (wcześniej wybranie kategorii do której będzie należał link):






Implementacja:
Podczas tworzenia kodu programu nie używałem na sztywno providera. Tak więc zamiast SqLiteConnection użyłem DbConnection. Pozwala to na szybkie przełączanie się między różnymi bazami danych. Klient zmieniając tylko odpowiednio ConnectionString może użyć własnej bazy danych. Zobaczmy na przykład kodu napisanego właśnie w takim stylu:

        public IList<Category> GetAllCategories()
        {
            IList<Category> categories = new List<Category>();
            using (DbConnection dbconn = dbProvider.CreateConnection())
            {
                using (DbCommand dbcmd = dbProvider.CreateCommand())
                {
                    dbconn.ConnectionString = connectionString.ConnectionString;
                    dbcmd.Connection = dbconn;
                    dbcmd.CommandText = @"SELECT IdCategory, CategoryName
                                            FROM Category";
                    dbconn.Open();
                    using (DbDataReader dr = dbcmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Category tmp = new Category();
                            tmp.CategoryName = (string)dr["CategoryName"];
                            tmp.IdCategory = int.Parse(dr["IdCategory"].ToString());
                            categories.Add(tmp);
                        }
                    }
                }
            }
            return categories;
        }

W całej aplikacji parametry przekazywane są poprzez obiekt DbParameter, unikamy w ten sposób zawiłych łączeń kodu, jak i to co idzie do bazy nie zawiera niebezpieczeństwa SQL injection (wstrzyknięcie złośliwego kodu). Przykład takiego kodu:

       public IList<Url> GetUrlsByCategoryId(int idCategory)
        {
            IList<Url> urls = new List<Url>();
            using (DbConnection dbconn = dbProvider.CreateConnection())
            {
                using (DbCommand dbcmd = dbProvider.CreateCommand())
                {
                    dbcmd.Connection = dbconn;
                    dbconn.ConnectionString = connectionString.ConnectionString;
                    dbcmd.CommandText = @"SELECT u.IdUrl, Url,Description
                                            FROM Url u JOIN UrlCategory uc ON u.IdUrl = uc.IdUrl JOIN Category c ON uc.IdCategory = c.IdCategory
                                            WHERE c.IdCategory = @IdCategory";
                    DbParameter idCategoryParameter = dbcmd.CreateParameter();
                    idCategoryParameter.DbType = DbType.Int32;
                    idCategoryParameter.ParameterName = "@IdCategory";
                    idCategoryParameter.Value = idCategory;
                    dbcmd.Parameters.Add(idCategoryParameter);
                    dbconn.Open();
                    using (DbDataReader dr = dbcmd.ExecuteReader())
                    {
                        while (dr.Read())
                        {
                            Url tmp = new Url();
                            tmp.IdUrl = int.Parse(dr["IdUrl"].ToString());
                            tmp.Description = (string)(dr["Description"] == DBNull.Value ? "" : dr["Description"]);
                            tmp.UrlString = (string)(dr["Url"] == DBNull.Value ? "" : dr["Url"]);
                            urls.Add(tmp);
                        }
                    }
                }
            }
            return urls;
        }


Wygląd elementów ListBox został zmieniony poprzez zastosowanie odpowiedniego stylu dla nich:

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Style x:Key="lbxItemStyle" TargetType="{x:Type ListBoxItem}">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="HorizontalContentAlignment" Value="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
        <Setter Property="VerticalContentAlignment" Value="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"/>
        <Setter Property="Padding" Value="2,0,0,0"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ListBoxItem}">
                    <Grid>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualStateGroup.Transitions>
                                    <VisualTransition GeneratedDuration="0:0:1"/>
                                </VisualStateGroup.Transitions>
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="MouseOver">
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.OpacityMask).(GradientBrush.GradientStops)[1].(GradientStop.Offset)" Storyboard.TargetName="contentPresenter">
                                            <EasingDoubleKeyFrame KeyTime="0" Value="1"/>
                                        </DoubleAnimationUsingKeyFrames>
                                        <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(Panel.Background).(GradientBrush.GradientStops)[1].(GradientStop.Offset)" Storyboard.TargetName="border">
                                            <EasingDoubleKeyFrame KeyTime="0" Value="0.293"/>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Disabled"/>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Border x:Name="border" BorderBrush="#FF93A3E5" BorderThickness="2" Margin="0" CornerRadius="10">
                            <Border.Background>
                                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                    <GradientStop Color="Black"/>
                                    <GradientStop Color="#FF92A2CA" Offset="1"/>
                                </LinearGradientBrush>
                            </Border.Background>
                            <ContentPresenter x:Name="contentPresenter" HorizontalAlignment="Center" VerticalAlignment="Center">
                                <ContentPresenter.OpacityMask>
                                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                        <GradientStop Color="Black" Offset="0"/>
                                        <GradientStop Color="White" Offset="1"/>
                                    </LinearGradientBrush>
                                </ContentPresenter.OpacityMask>
                            </ContentPresenter>
                        </Border>
                    </Grid>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsSelected" Value="true">
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}"/>
                        </Trigger>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsSelected" Value="true"/>
                                <Condition Property="Selector.IsSelectionActive" Value="false"/>
                            </MultiTrigger.Conditions>
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
                        </MultiTrigger>
                        <Trigger Property="IsEnabled" Value="false">
                            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!-- Resource dictionary entries should be defined here. -->
</ResourceDictionary>


Dodatkowe uwagi:
Należy pamiętać, że korzystając z providera który nie jest dostępny domyślnie na platformie .NET należy dodać go do odpowiedniej sekcji w pliku App.config bądź też bezpośrednio zmodyfikować plik Machine.config na maszynie klienta. W obu rozwiązaniach należy dodać następujący wpis:

  <system.data>
    <DbProviderFactories>
      <add name="SQLite Data Provider" invariant="System.Data.SQLite" description=".Net Framework Data Provider for SQLite"
           type="System.Data.SQLite.SQLiteFactory, System.Data.SQLite" />
    </DbProviderFactories>
  </system.data>


Kolejnym ważnym punktem jest udostępnienie wraz z exe aplikacji odpowiedniej biblioteki umożliwiającej korzystanie z SqLite:



Co brakuje w projekcie a przydałoby się dorobić? Oczywiście jest cała lista takich rzeczy (które są na tzw. liście do dokończenia kiedyś :) ). Projekcik miał za zadanie pokazać jak posługiwać się bazą SqLite brakuje więc w nim kilku moim zdaniem przydatnych rzeczy:
- możliwosć dodawania do wielu kategorii
- skórki dla grida
- większa obsługa klawiatury w aplikacji (szybciej coś wyklikać na klawiaturze niż namierzyć myszką :) )
- logowanie do aplikacji
- eksport linków do pliku itp.
- poprawienie wizualnego wyglądu aplikacji poprzez zastosowanie motywów (kiedyś opiszę jak je wykorzystuję w aplikacji Silverlight którą ostatnio tworzyłem)

Projekt w obecnym stanie można pobrać i przetestować stąd.
Życzę udanego czytania kodu jak i eksperymentowania poprzez dodawanie własnych modułów/rozszerzanie istniejącej funkcjonalności.

Oczywiście w razie pytań służę pomocą.

Brak komentarzy:

Prześlij komentarz