czwartek, 8 listopada 2012

Prezent od Microsoft - Darmowa książka o programowaniu w Windows 8

Microsoft konsekwentnie kontynuuje taktykę ściągania nie .NETowych programistów - udostępnił darmową książkę w ramach Microsoft Press: "Programming Windows 8 Apps with HTML, CSS, and JavaScript". Jest ona dostępna pod adresem:
http://blogs.msdn.com/b/microsoft_press/archive/2012/10/29/free-ebook-programming-windows-8-apps-with-html-css-and-javascript.aspx
Nie do końca jestem przekonany do tego, że HTML5, JavaScript i CSS jest tym kierunkiem, w którym powinny podążać systemy operacyjne. Wg mnie te technologie są chaotyczne, mają pełno złych zaszłości historycznych i są ciężkie w utrzymaniu. (Nie)stety nie tylko Microsoft podąża w tą stronę - Apple, Google również mocno w nią idą. 
A wy co o tym wszystkim sądzicie?

wtorek, 30 października 2012

Serializacja dla .NET 4.5 oraz Windows Runtime przy pomocy Sharpserializer

Trochę mnie nie było, dawno już nie pisałem - ten post będzie dla mnie nietypowy - krótki. Mam nadzieję, że to będzie jego zaleta.
W swoim projekcie-po-godzinach do serializacji danych używam biblioteki SharpSerializer. Projekt ma środowiska klienckie napisane w Silverlight i Windows Phone. Nie ma w nich klasy BinaryFormatter przez co bez stosowania zewnętrznych bibliotek trzeba by stosować sztuczki z serializacją poprzez mechanizm DataContract z WCF (więcej szczegółów na blogu Damona Payne'a). Nie jest to zbyt wygodne wg mnie.
SharpSerializer pozwala w prosty, wygodny i efektywny sposób serializować dane do postaci binarnej. 
Dlaczego tak nagle o tym piszę? W tym tygodniu zacząłem przenosić kod projektu na .NET 4.5 i Windows Runtime. Niestety nie zostały do tej pory wypuszczone wersje na te środowiska.
Na szczęście ze strony można ściągnąć kody źródłowe.
Pobrałem je, przekonwertowałem, poprawiłem część rzeczy, przekompilowałem i okazało się, że wszystko wygląda jakby działało.
Efekt moich prac możecie pobrać: tutaj (src + dll).
Więcej informacji na temat SharpSerializer pod linkami:
- http://www.sharpserializer.com/en/tutorial/index.html
- http://www.codeproject.com/Articles/76530/XML-Serialization-of-Generic-Dictionary-Multidimen
- http://www.codeproject.com/Articles/240621/How-to-serialize-data-effectively-Custom-serializa
- http://www.codeproject.com/Articles/116020/Binary-Serialization-to-Isolated-Storage-in-Silver
Zachęcam do zabawy z SharpSerializerem, naprawdę dobra biblioteka.

niedziela, 22 lipca 2012

[EN] Windows Phone - skinnable application

1. Introduction
Hi
Today, I would like to present my concept of making skinnable application in Windows Phone 7.1. Making a skinnable application in Windows Phone might be a little bit tricky. Unfortunately, WP7 doesn't support DynamicResource keyword which is a basic tool for making skins in WPF. In order to overcome this inconvenience I decided to write a SkinManager for WindowsPhone.
2. Base view
As I mentioned before, there is no support for using dynamic resources in WP7, therefore, in order to force a control to change style, we have to assign this style in code behind. It is rather obvious that in a single window/control there might be quite a lot of other controls whose style also should be changed. That is why, I decided to write controls iterator first.
public class ViewsEnumerator : IEnumerator<FrameworkElement>
    {
        
        private Stack<FrameworkElement> _frameworkElementsStack = new Stack<FrameworkElement>();
        private FrameworkElement _current;
        private readonly FrameworkElement _startingElement;
        public ViewsEnumerator(FrameworkElement stargingElement)
        {
            _startingElement = stargingElement;
        }
        public void Dispose()
        {
            _current = null;
            _frameworkElementsStack.Clear();
            _frameworkElementsStack = null;
        }
 

        #region Implementation of IEnumerator

        public bool MoveNext()
        {
            if (_current == null)
            {
                _current = _startingElement;
                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(_current); i++)
                    _frameworkElementsStack.Push((FrameworkElement)VisualTreeHelper.GetChild(_current, i));
            }
            else
            {

                if (_frameworkElementsStack.Count == 0) return false;
                _current = _frameworkElementsStack.Pop();

                for (int i = 0; i < VisualTreeHelper.GetChildrenCount(_current); i++)
                    _frameworkElementsStack.Push((FrameworkElement)VisualTreeHelper.GetChild(_current, i));
                return true;
            }
            return true;
        }

        public void Reset()
        {
            _current = null;
            _frameworkElementsStack.Clear();
            _frameworkElementsStack = null;
        }

        public FrameworkElement Current
        {
            get { return _current; }
        }

        object IEnumerator.Current
        {
            get { return Current; }
        }

        #endregion
    }
In this iterator I used DFS algorithm for searching elements in a tree. Thanks to this piece of code I can take advantage of LINQ's benefits. In the next step I created base classes for views.
public interface IViewBase : IEnumerable<FrameworkElement>
    {
        
    }

 public class ViewBase : UserControl, IViewBase
    {
       #region Implementation of IEnumerable

        /// <summary>
        /// Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator<FrameworkElement> GetEnumerator()
        {
            return new ViewsEnumerator(this);
        }

        /// <summary>
        /// Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion
    }

3. SkinManager class
Having prepared basis for skinnable control, let me introduce a custom attribute called the SkinnablePartAttribute
[AttributeUsage(AttributeTargets.Class)]
    public class SkinnablePartAttribute : Attribute
    {
        public string ThemePartName { get; set; }

    }
This attribute should decorate custom controls which should have an ability to change theme/skin. As you can see, the SkinnablePartAttribute has a ThemePartName property which holds information about a name of ResourceDictionary where styles for the given control ought to be searched. Now, it is time to take a look at a SkinManager class. The SkinManager class has one public function - ApplySkin
public static void ApplySkin<TView>(TView view, string skinName)
           where TView : FrameworkElement, IViewBase
        {

            foreach (IViewBase control in view.Where(val => val.ImplementsInterfaceOf<IViewBase>()))
            {
                var assemblyName = control.GetType().Assembly.FullName;
                Uri parsedUri;
                var attribute = control.GetType().GetCustomAttributes(typeof(SkinnablePartAttribute), true).SingleOrDefault();
                if (attribute != null && Uri.TryCreate(string.Format("/{0};component/Themes/{1}/{2}.xaml",assemblyName.Substring(0, assemblyName.IndexOf(",", StringComparison.Ordinal)), skinName,(attribute as SkinnablePartAttribute).ThemePartName), UriKind.Relative, out parsedUri))
                    ApplySkin(control, parsedUri);
                else
                    Debug.WriteLine(string.Format("Could not find {0}/{1}", skinName,(attribute as SkinnablePartAttribute).ThemePartName));
                
            }
        }
This function iterates through all controls which implement a IViewBase interface - in other words, the function iterates through our custom controls. If a control is decorated with the SkinnablePartAttribue, a new Uri is created based on the information from the ThemePartName property and from the skinName parameter(I assumed that all dictionaries are located in the Themes directory). In the final step, private function also known as the AppySkin function is called. This function looks this way:
private static void ApplySkin<TView>(TView view, Uri uri, int mergedDictionaryIndex = 0)
            where TView :  IViewBase
        {
            var castedView = view as FrameworkElement;
            if(castedView == null) return;

            var resourceMergedDictionary = castedView.Resources.MergedDictionaries[mergedDictionaryIndex];
            var beforeClearDictionary = resourceMergedDictionary.ToDictionary(key => key.Value, val => new { key = val.Key, value = val.Value });
            castedView.Resources.MergedDictionaries.Remove(resourceMergedDictionary);
            resourceMergedDictionary.Clear();

            var newStyleDictionary = new ResourceDictionary
            {
                Source = uri
            };

            castedView.Resources.MergedDictionaries.Insert(mergedDictionaryIndex, newStyleDictionary);
            foreach (var control in view.Where(val => val.Style != null && !val.ImplementsInterfaceOf<IViewBase>()).Where(control =>
                beforeClearDictionary.ContainsKey(control.Style) && newStyleDictionary[beforeClearDictionary[control.Style].key] != null))
                control.Style = newStyleDictionary[beforeClearDictionary[control.Style].key] as Style;

            beforeClearDictionary.Clear();

        }
Here, all the magic begins. In the first step, current ResourceDictionary is transformed into the standard dictionary. The key of this dictionary is a value of style – a reference to the style object. In the next step, I created a new ResourceDictionary based on the Uri parameter passed to this function. In the last step, another loop over controls is performed. However, this time only controls with the assigned Style property are taken into consideration. Then, if the newStyleDictionary contains a key of given control style we are ready to swap styles.
control.Style = newStyleDictionary[beforeClearDictionary[control.Style].key] as Style;
Entire listing of SkinManager is presented below
public static class SkinManager
    {
        private static void ApplySkin<TView>(TView view, Uri uri, int mergedDictionaryIndex = 0)
            where TView :  IViewBase
        {
            var castedView = view as FrameworkElement;
            if(castedView == null) return;

            var resourceMergedDictionary = castedView.Resources.MergedDictionaries[mergedDictionaryIndex];
            var beforeClearDictionary = resourceMergedDictionary.ToDictionary(key => key.Value, val => new { key = val.Key, value = val.Value });
            castedView.Resources.MergedDictionaries.Remove(resourceMergedDictionary);
            resourceMergedDictionary.Clear();

            var newStyleDictionary = new ResourceDictionary
            {
                Source = uri
            };

            castedView.Resources.MergedDictionaries.Insert(mergedDictionaryIndex, newStyleDictionary);
            foreach (var control in view.Where(val => val.Style != null && !val.ImplementsInterfaceOf<IViewBase>()).Where(control =>
                beforeClearDictionary.ContainsKey(control.Style) && newStyleDictionary[beforeClearDictionary[control.Style].key] != null))
                control.Style = newStyleDictionary[beforeClearDictionary[control.Style].key] as Style;

            beforeClearDictionary.Clear();

        }
        public static void ApplySkin<TView>(TView view, string skinName)
           where TView : FrameworkElement, IViewBase
        {

            foreach (IViewBase control in view.Where(val => val.ImplementsInterfaceOf<IViewBase>()))
            {
                var assemblyName = control.GetType().Assembly.FullName;
                Uri parsedUri;
                var attribute = control.GetType().GetCustomAttributes(typeof(SkinnablePartAttribute), true).SingleOrDefault();
                if (attribute != null && Uri.TryCreate(string.Format("/{0};component/Themes/{1}/{2}.xaml",assemblyName.Substring(0, assemblyName.IndexOf(",", StringComparison.Ordinal)), skinName,(attribute as SkinnablePartAttribute).ThemePartName), UriKind.Relative, out parsedUri))
                    ApplySkin(control, parsedUri);
                else
                    Debug.WriteLine(string.Format("Could not find {0}/{1}", skinName,(attribute as SkinnablePartAttribute).ThemePartName));
                
            }
        }
4. Example of usage
In order to use the SkinManager it is essential to make some preparation. First of all, you have to create Themes directory in a project. In this directory there must be separate subdirectories for every skin you would like to use. What is more, in these subdirectories you should place all ResourceDictionaries specified in the SkinnablePartAttribute. A sample directory structure is shown in the picture below:
The very last step is to make controls (which should be skinnable) inherit from ViewBase. Here is example of usage
namespace Joomanji.Chat.Client.Views
{
    [SkinnablePart(ThemePartName = "ChatWindowSkin")]
    public partial class ChatWindowView : ViewBase
    {
        public ChatWindowView()
        {
            SkinManager.ApplySkin(this, "Red"); // change skin
        }
    }   
}
and screenshots of the same view with different skin
5. Controls with dynamically changing content
After some test, it turned out that the mechanism presented above has problems with controls which change their content dynamically. Let me clarify - styling grids, list boxes etc. work fine, however changing row styles and listboxitem styles are not working properly. In order to overcome this problem you must define a style for a specific target type. Here is an example of usage with the listbox:
<Style TargetType="ListBox">
                        <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
                        <Setter Property="VerticalAlignment" Value="Stretch"/>
                        <Setter Property="ItemTemplate" Value="{StaticResource GeneralChatMessageTemplate}"/>
                    </Style>

niedziela, 10 czerwca 2012

Windows Phone - wyświetlanie błędów walidacji

Kilka miesięcy temu w tym poście http://premium-hands.blogspot.com/2011/11/walidacja.html przedstawiałem różne sposoby walidacji jakie są dostępne na platformie Silverlight. Jako, że ostatnio mam więcej czasu aby pisać jakieś proste aplikacje pod Windows Phonem potrzebowałem mechanizmu walidacji działającego pod tą platformą. Według MSDN-u wszystkie metody walidacji przedstawione we wspomnianym wcześniej poście powinny działać. Niestety po przerzuceniu mechanizmu z Silverlighta na Windows Phona okazało się, że błędy walidacji nie zostają wyświetlone na interfejsie. Po przeszukaniu kilku for oraz blogów dowiedziałem się, że przyczyną takiego stanu rzeczy jest brak obsługi reakcji na błędy w templacie TextBoxów (i innych elementów typu input). Zatem aby wyświetlić błędy walidacji, pozostaje nam zmodyfikować istniejący template TextBox-a. Zacznijmy od wyciągnięcia standardowego templatu TextBox-a z Windows Phona. W tym celu możemy użyć aplikacji Expression Blend. Otwórzmy w niej jakikolwiek projekt typu Windows Phone Application, w którym na kontrolce mamy naniesionego Textboxa. W moim przypadku, po wczytaniu projektu do Blenda ekran wygląda w następujący sposób
Następnie zaznaczamy w Blendzie TextBox-a i z opcji znajdujących się w lewym górnym rogu aktualnej zakładki wybieramy TextBox->Edit Template-> Edit Copy.
W oknie, które się pojawi klikamy OK. Następnie możemy przejść do widoku XAML-a, w sekcji phone:PhoneApplicationPage.Resources powinien pojawić się nam nowy styl wyglądający w ten sposób
<ControlTemplate x:Key="PhoneDisabledTextBoxTemplate" TargetType="TextBox">
   <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
  </ControlTemplate>
<Style x:Key="TextBoxStyle1" TargetType="TextBox">
   <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
   <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
   <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/>
   <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/>
   <Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/>
   <Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/>
   <Setter Property="SelectionForeground" Value="{StaticResource PhoneTextBoxSelectionForegroundBrush}"/>
   <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
   <Setter Property="Padding" Value="2"/>
   <Setter Property="Template">
    <Setter.Value>
     <ControlTemplate TargetType="TextBox">
      <Grid Background="Transparent">
       <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="CommonStates">
         <VisualState x:Name="Normal"/>
         <VisualState x:Name="MouseOver"/>
         <VisualState x:Name="Disabled">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Collapsed</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Visible</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </VisualState>
         <VisualState x:Name="ReadOnly">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Collapsed</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0">
             <DiscreteObjectKeyFrame.Value>
              <Visibility>Visible</Visibility>
             </DiscreteObjectKeyFrame.Value>
            </DiscreteObjectKeyFrame>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="DisabledOrReadonlyBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="DisabledOrReadonlyContent">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxReadOnlyBrush}"/>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </VisualState>
        </VisualStateGroup>
        <VisualStateGroup x:Name="FocusStates">
         <VisualState x:Name="Focused">
          <Storyboard>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/>
           </ObjectAnimationUsingKeyFrames>
           <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder">
            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/>
           </ObjectAnimationUsingKeyFrames>
          </Storyboard>
         </VisualState>
         <VisualState x:Name="Unfocused"/>
        </VisualStateGroup>
       </VisualStateManager.VisualStateGroups>
       <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
        <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
       </Border>
       <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
        <TextBox x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/>
       </Border>
      </Grid>
     </ControlTemplate>
    </Setter.Value>
   </Setter>
  </Style>
W ten oto sposób udało nam się wydobyć domyślny styl oraz template TextBoxa w Windows Phonie. W kolejnym kroku musimy zmodyfikować ten template w taki sposób, aby zaczął reagować na zmiany stanu walidacji. Zacznijmy od sprawdzenia jakie stany wizualne może obsługiwać TextBox. W tym celu musimy przeglądnąć metadane TextBoxa - możemy to zrobić klikając PPM na klasie TextBox-a i wybierając Go To Definition. Naszym oczom powinno ukazać się coś takiego.
Widzimy, że klasa TextBox jest opatrzona atrybutami TemplateVisualState, nas interesują najbardziej trzy z tych atrybutów:
  • [TemplateVisualState(GroupName = "ValidationStates", Name = "Valid")]
  • [TemplateVisualState(GroupName = "ValidationStates", Name = "InvalidFocused")]
  • [TemplateVisualState(GroupName = "ValidationStates", Name = "InvalidUnfocused")]
Mówią nam one, że w przypadku gdy wystąpią błędy walidacji nasza kontrolka przechodzi w stan InvalidFocused lub InvalidUnfocused. Natomiast, w przypadku gdy nie ma błędów znajduje się ona w stanie Valid. Analizując kod domyślnego templatu TextBoxa widzimy, że stany z grupy ValidationStates nie są obsługiwane - czas zatem to zmienić. Po pierwsze do templatu musimy dorzucić elementy, które będą wyświetlały błędy walidacji. Zmieńmy zatem opakowujący wszystko element Grid na StackPanel, a następnie dodajmy element typu TextBlock oraz Border. Elementy wyświetlające błędy walidacji musimy umieścić w jednym kontenerze z elementami odpowiadającymi za wyświetlanie tekstu. Zatem musimy je opakować w Grida. Po zastosowaniu powyższych wskazówek nasz template będzie wyglądał w następujący sposób
<StackPanel>
...
...
...
 <Grid>
   <Grid.RowDefinitions>
       <RowDefinition Height="Auto"></RowDefinition>
       <RowDefinition Height="Auto"></RowDefinition>
   </Grid.RowDefinitions>
 <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
       <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch"   Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
 </Border>
  <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
      <TextBox x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/>
 </Border>
     <Border x:Name="ValidationBorder" Grid.RowSpan="2" BorderBrush="Red" BorderThickness="2" Visibility="Collapsed"  >
 </Border>
     <TextBlock x:Name="ValidationMessage" Foreground="Red" FontSize="16" Visibility="Collapsed" Grid.Row="1" Text="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource TemplatedParent}}"  Padding="0,0,0,2" Margin="10,-14,0,0"  />
</Grid>
</StackPanel>
Dodanie elementów wyświetlających błędy to nie wszystko, musimy jeszcze określić kiedy nasze elementy mają być widoczne. W chwili obecnej ich właściwość Visibility jest ustawiona na Collapsed. Zatem elementy te nie będą widoczne, jak również nie będą zajmowały miejsca na interfejsie. Jak już wcześniej wspomniałem kontrolka TextBoxa w przypadku wystąpienia błędów walidacji przejdzie do stanów InvalidFocused lub InvalidUnfocused. Pozostaje nam zatem zareagować na przejście kontrolki w te stany i odpowiednio zmodyfikować właściwość Visibility bordera oraz textblocka. W tym celu posłużymy się VisualStateManagerem oraz animacjami. Do naszego templatu dodajmy następujący wpis
<VisualStateGroup x:Name="ValidationStates">
                                <VisualState x:Name="Valid" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidFocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidUnfocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
W ten oto sposób zdefiniwaliśmy w XAML-u grupę (VisualStateGroup ) o nazwie ValidationStates - zauważmy, że nazwa grupy jest taka sama jak nazwa grupy z atrybutów klasy TextBox. Dodaliśmy również trzy stany wizualne (VisualState), których nazwy również odpowiadają nazwą zdefiniowanym w atrubytach klasy TextBox. Od tej chwili gdy kontrolka TextBoxa będzie posiadała błędy walidacji, zostanie odpalona animacja zmieniająca właściwość Visibility bordera "ValidationBorder" oraz TextBlocka "ValidationMessage" na Visible. Ostatecznie cały gotowy template będzie wyglądał w następujący sposób
<ControlTemplate x:Key="PhoneDisabledTextBoxTemplate" TargetType="TextBox">
        <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch" Margin="{StaticResource PhoneTextBoxInnerMargin}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
    </ControlTemplate>
    <Style TargetType="TextBox">
        <Setter Property="FontFamily" Value="{StaticResource PhoneFontFamilyNormal}"/>
        <Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMediumLarge}"/>
        <Setter Property="Background" Value="{StaticResource PhoneTextBoxBrush}"/>
        <Setter Property="Foreground" Value="{StaticResource PhoneTextBoxForegroundBrush}"/>
        <Setter Property="BorderBrush" Value="{StaticResource PhoneTextBoxBrush}"/>
        <Setter Property="SelectionBackground" Value="{StaticResource PhoneAccentBrush}"/>
        <Setter Property="SelectionForeground" Value="{StaticResource PhoneTextBoxSelectionForegroundBrush}"/>
        <Setter Property="BorderThickness" Value="{StaticResource PhoneBorderThickness}"/>
        <Setter Property="Padding" Value="2"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="TextBox">
                    <StackPanel Background="Transparent">
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal"/>
                                <VisualState x:Name="MouseOver"/>
                                <VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="ReadOnly">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Collapsed</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0">
                                                <DiscreteObjectKeyFrame.Value>
                                                    <Visibility>Visible</Visibility>
                                                </DiscreteObjectKeyFrame.Value>
                                            </DiscreteObjectKeyFrame>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="DisabledOrReadonlyBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground" Storyboard.TargetName="DisabledOrReadonlyContent">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxReadOnlyBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="FocusStates">
                                <VisualState x:Name="Focused">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBackgroundBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush" Storyboard.TargetName="EnabledBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource PhoneTextBoxEditBorderBrush}"/>
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="Unfocused"/>
                            </VisualStateGroup>
                            <VisualStateGroup x:Name="ValidationStates">
                                <VisualState x:Name="Valid" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Collapsed" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidFocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                                <VisualState x:Name="InvalidUnfocused" >
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationBorder">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility" Storyboard.TargetName="ValidationMessage">
                                            <DiscreteObjectKeyFrame KeyTime="0" Value="Visible" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto"></RowDefinition>
                                <RowDefinition Height="Auto"></RowDefinition>
                            </Grid.RowDefinitions>
                            <Border x:Name="EnabledBorder" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Margin="{StaticResource PhoneTouchTargetOverhang}">
                                <ContentControl x:Name="ContentElement" BorderThickness="0" HorizontalContentAlignment="Stretch"   Padding="{TemplateBinding Padding}" VerticalContentAlignment="Stretch"/>
                            </Border>
                            <Border x:Name="DisabledOrReadonlyBorder" BorderBrush="{StaticResource PhoneDisabledBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="Transparent" Margin="{StaticResource PhoneTouchTargetOverhang}" Visibility="Collapsed">
                                <TextBox x:Name="DisabledOrReadonlyContent" Background="Transparent" Foreground="{StaticResource PhoneDisabledBrush}" FontWeight="{TemplateBinding FontWeight}" FontStyle="{TemplateBinding FontStyle}" FontSize="{TemplateBinding FontSize}" FontFamily="{TemplateBinding FontFamily}" IsReadOnly="True" SelectionForeground="{TemplateBinding SelectionForeground}" SelectionBackground="{TemplateBinding SelectionBackground}" TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="{TemplateBinding TextWrapping}" Text="{TemplateBinding Text}" Template="{StaticResource PhoneDisabledTextBoxTemplate}"/>
                            </Border>
                            <Border x:Name="ValidationBorder" Grid.RowSpan="2" BorderBrush="Red" BorderThickness="2" Visibility="Collapsed"  >
                            </Border>
                            <TextBlock x:Name="ValidationMessage" Foreground="Red" FontSize="16" Visibility="Collapsed" Grid.Row="1" Text="{Binding (Validation.Errors)[0].ErrorContent, RelativeSource={RelativeSource TemplatedParent}}"  Padding="0,0,0,2" Margin="10,-14,0,0"  />

                        </Grid>

                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
Tak natomiast przedstawiają się błędy walidacji wyświetlone na TextBoxach

Windows Phone - własny DataTemplateSelector

Pisząc moją małą aplikację pod Windows Phone po raz kolejny natknąłem się na problem. Pod WP7 nie ma tak przydatnej rzeczy jaką jest DataTemplateSelector znany nam bardziej, lub mniej z Silverlighta oraz WPF-a.Na szczęście napisane własnego DataTemplateSelector-a nie jest specjalnie skomplikowane. Jak zwykle w takich przypadkach liczy się pomysł - jak dobrze, że jest Google. Nasz customowy DataTemplateSelector zostanie oparty o kontrolkę ContentControl. Po pierwsze stwórzmy klasę bazową DataTemplateSelectora dziedziczącą po ContentControl,w której przeciążamy funkcję OnContentChanged,
public abstract class AbstractDataTemplateSelector : ContentControl
    {
        public abstract DataTemplate SelectTemplate(object item, DependencyObject container);
        
        protected override void OnContentChanged(object oldContent, object newContent)
        {
            base.OnContentChanged(oldContent, newContent);

            ContentTemplate = SelectTemplate(newContent, this);
        }
    }
Klasa ta będzie klasą z której będą wywodzić się wszystkie nasze TemplateSelectory. Załóżmy, że chcemy zbudować DataTemplateSelector, który będzie obsługiwał aplikację chata. Powinien on zatem reagować na dwa typy elementów:
  • wiadomości przychodzące
  • wiadomości wychodzące
Stwórzmy zatem "wyspecjalizowany" DataTemplateSelector, który w zależności od typu wiadomości zwróci odpowiedni DataTemplate.
public class ChatMessageTemplateSelector : AbstractDataTemplateSelector
    {
        public DataTemplate Out
        {
            get;
            set;
        }

        public DataTemplate In
        {
            get;
            set;
        }

        public override DataTemplate SelectTemplate(object item, DependencyObject container)
        {
            ChatMessage message = item as ChatMessage;
            if (message != null)
            {
                switch (message.MessageDirection)
                {
                    case MessageDirection.In:
                        return In;
                    case MessageDirection.Out:
                        return Out;
                    default:
                        return Out;
                }
            }
            return In;
        }
    }
W klasie ChatMessageTemplateSelector przeciążyliśmy metodę SelectTemplate. W metodzie tej, na podstawie zbindowanego obiektu (parametr item) określamy jaki template powinniśmy zwrócić. Templaty do wiadomości przychodzących oraz wychodzących zostały zdefiniowane jako propertisy.
  • Out - template wiadomości wychodzącej
  • In - template wiadomości przychodzącej
Pozostaje nam tylko zdefiniować wymienione wyżej templaty. Nie będziemy robić tego w kodzie, ale w XAML-u. Nasz nowo stworzony DataTemplateSelector wykorzytamy w ListBox-ie
<ListBox  ItemsSource="{Binding MessageList,Mode=TwoWay}" Grid.ColumnSpan="2" HorizontalContentAlignment="Stretch" VerticalAlignment="Stretch"
                     ItemTemplate="{StaticResource GeneralChatMessageTemplate}" >
Jako, że nasz ChatMessageTemplateSelector jest tak naprawdę obiektem typu ContentControl, zatem możemy go zbindować do właściwości ItemTemplate ListBox-a. Zasób GeneralChatMessageTemplate wygląda w następujący sposób
<DataTemplate x:Key="GeneralChatMessageTemplate">
            <Client:ChatMessageTemplateSelector HorizontalContentAlignment="Stretch" Content="{Binding}" In="{StaticResource IncomingChatMessageTemplate}" Out="{StaticResource OutcomingChatMessageTemplate}">
            </Client:ChatMessageTemplateSelector>
        </DataTemplate>
Jak widać jest to obiekt typu ChatMessageTemplateSelector, którego właściwość Content jest zbindowana do aktualnego kontekstu (pojedyncze słowo kluczowe Binding). Widzimy również, że ustawiliśmy wartości właściwości odpowiadających za templaty poszczególnych wiadomości (In="{StaticResource IncomingChatMessageTemplate}" Out="{StaticResource OutcomingChatMessageTemplate}"). Wspomniane templaty wyglądają w następujący sposób
<DataTemplate x:Key="IncomingChatMessageTemplate">
            <Border BorderThickness="0,0,0,1" BorderBrush="#2d3550" >
                <Grid Background="#181c2a" HorizontalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal">
                        <Border BorderThickness="1" BorderBrush="White" Margin="5,5,0,0">
                            <Image Stretch="None"  Width="30" Height="30" MaxWidth="30" MinHeight="30" MaxHeight="30"  Source="..\avatar_4934.bmp"></Image>
                        </Border>
                        <TextBlock Padding="5,5,5,0" Foreground="White" FontFamily="Segoe UI, Tahoma" FontSize="12" FontWeight="Bold" TextTrimming="WordEllipsis"  TextAlignment="Left" VerticalAlignment="Center" HorizontalAlignment="Left"  Text="{Binding Sender.UserName,Mode=TwoWay}"></TextBlock>
                    </StackPanel>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="10" Padding="5,5,5,0" Foreground="#8e929f" TextTrimming="WordEllipsis"  Grid.Column="1" TextAlignment="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Text="{Binding SendTime, Mode=TwoWay, StringFormat='\{0:dd.MM HH:mm:ss\}'}"></TextBlock>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="12" Margin="5,0,0,0" TextWrapping="Wrap" Grid.ColumnSpan="2" Grid.Row="1" Text="{Binding Message,Mode=TwoWay}" ></TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
<DataTemplate x:Key="OutcomingChatMessageTemplate">
            <Border BorderThickness="0,0,0,1" BorderBrush="#2d3550">
                <Grid HorizontalAlignment="Stretch">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition></RowDefinition>
                        <RowDefinition></RowDefinition>
                    </Grid.RowDefinitions>
                    <StackPanel Orientation="Horizontal">
                        <Border BorderThickness="1" BorderBrush="White" Margin="5,5,0,0">
                            <Image Stretch="None"  Width="30" Height="30" MaxWidth="30" MinHeight="30" MaxHeight="30"  ></Image>
                        </Border>
                        <TextBlock Padding="5,5,5,0" Foreground="#8e929f" FontFamily="Segoe UI, Tahoma" FontSize="12" FontWeight="Bold" TextTrimming="WordEllipsis"  TextAlignment="Left" VerticalAlignment="Center" HorizontalAlignment="Left"  Text="{Binding Sender.UserName,Mode=TwoWay}"></TextBlock>
                    </StackPanel>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="10" Padding="5,5,5,0" Foreground="#8e929f" TextTrimming="WordEllipsis"  Grid.Column="1" TextAlignment="Right" VerticalAlignment="Center" HorizontalAlignment="Right" Text="{Binding SendTime, Mode=TwoWay, StringFormat='\{0:dd.MM HH:mm:ss\}'}"></TextBlock>
                    <TextBlock FontFamily="Segoe UI, Tahoma" FontSize="12" Margin="5,0,0,0" TextWrapping="Wrap" Grid.ColumnSpan="2" Grid.Row="1" Text="{Binding Message,Mode=TwoWay}" ></TextBlock>
                </Grid>
            </Border>
        </DataTemplate>
Od teraz gdy do ListBox-a zostanie dodany nowy item, pierwszą rzeczą jaka się odpali będzie funkcja OnContentChanged. W funkcji tej AbstractDataTemplateSelector na podstawie nowo przybyłego itemu ustawi właściwość ContentTemplate, na taki template jaki zwróci mu funkcja SelectTemplate. Wynik działania ChatMessageTemplateSelector wygląda w następujący sposób

poniedziałek, 7 maja 2012

Tworzenie bootstrappera aplikacji przy użyciu CaliburnMicro

Witam po długiej przerwie. W dzisiejszym wpisie postaram się krótko opisać w jaki sposób stworzyć bootstrapper aplikacji przy użyciu CaliburnMicro. Ponadto przedstawię w jaki sposób skonfigurować bootstrapper w taki sposób, aby Caliburn wykorzystywał nasz własny kontener IOC.

1. Wstęp

CaliburnMicro jest to framework MVVM, który w znacznym stopniu ułatwia i przyśpiesza pisanie aplikacji pod WPF,Silverlight,WindowsPhone oraz WinRT. Zdecydowałem się poznać ten framework z uwagi na jego przenośność na różne platformy. Wcześniej całkiem sporo czasu poświęciłem Prismowi, jednakże z powodu licznych problemów z jego działaniem pod WindowsPhonem chciałem spróbować czegoś nowego.

2. Bootstrapper - Silverlight

Bootstrapper jest to ogólnie rzecz biorąc klasa inicjalizująca całą aplikację. W trakcie odpalania bootstrappera na ogół konfiguruje się kontener IOC(rejestruje wszystkie potrzebne typu), inicjalizuje się połączenie z serverem, oraz odpala się główne okno aplikacji - tzw. Shella. Dlatego też musimy odchudzić plik App.xaml.cs i zostawić w nim jedynie konstruktor wraz z wywołaniem funkcji InitializeComponents()
 public partial class App : Application
    {
        public App()
        {          
            InitializeComponent();
        }
    }
Zacznijmy od utworzenia nowego projektu typu SilverlightApplication, do którego dodajemy referencje do dll-ek Cliburna. Możemy to zrobić poprzez NuGetta (niestety u mnie one nie działały :D), jak również możemy je ściągnąć z oficjalnej strony projektu http://caliburnmicro.codeplex.com/releases/view/81466. Następnie stwórzmy główne okno aplikacji(wspomnianego wcześniej Shella) oraz jego viewmodel. W tym celu dodajemy do projektu Silverlight User Control o nazwie ShellView
oraz nową klasę ShellViewModel.
Dorzućmy jeszcze jakiegoś TextBox-a do naszego ShellView, tak aby mieć pewność, że rzeczywiście odpowiednie okno jest widoczne przy starcie aplikacji
<UserControl x:Class="Loginv2.ShellView"
    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:cal="clr-namespace:Caliburn.Micro;assembly=Caliburn.Micro" mc:Ignorable="d" Height="516" Width="792">

    <Grid x:Name="LayoutRoot"  >
        <TextBox Text="oto jest shell"/>
    </Grid>
</UserControl>
W kolejnym kroku musimy stworzyć właściwy bootstrapper - dodajmy nową klasę o nazwie SilverlightBootstrapper, która dziedziczyć będzie po klasie Bootstrapper
namespace Client.Silverlight
{
    public class SilverlightBootstrapper : Bootstrapper<ShellViewModel>
    {
      
    }
}
Klasa Bootstrapper jest to klasa dostarczona przez CaliburnMicro, generyczny parametr T określa nam viewmodel na podstawie którego framework będzie wyszukiwał odpowiedni widok shella ze swojego wbudowanego kontenera IOC (Caliburn podczas uruchomienia aplikacji rejestruje dostępne typu w kontenerze). Zgodnie z domyślną konwencją, CaliburnMicro jako główne okno aplikacji ustawi widok, który nazywać się będzie ShellView. W celu odpalenia naszego bootstrappera musimy jeszcze umieścić go w zasobach aplikacji. Zmodyfikujmy zatem plik App.xaml aby wyglądał w następujący sposób
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:Silverlight="clr-namespace:Client.Silverlight" x:Class="Client.Silverlight.App"
             >
    <Application.Resources>
        <Silverlight:SilverlightBootstrapper x:Key="Bootstrapper"></Silverlight:SilverlightBootstrapper>
    </Application.Resources>
</Application>
Kompilując, a następnie uruchamiając nasz projekt naszym oczom powinien ukazać się następujący widok


2.1 Własny kontener IOC

Powyżej przedstawiłem w jaki sposób stworzyć najprostszą wersję bootstrapera z wykorzystaniem CaliburnMicro oraz jego domyślnego kontenera IOC. Jednakże najczęściej jest tak, że w aplikacji wykorzystujemy już jakiś bardziej zaawansowany kontener i po prostu nie chcemy wykorzystywać jednego kontenera do "rozwiązywania" widoków, a drugiego do pozostałych rzeczy. Dlatego też pokaże teraz w jaki sposób skonfigurować napisany wcześniej bootstrapper tak aby widoki były wyciągane z naszego kontenera IOC. Zacznijmy do dodania do naszego projektu referencji do NInjecta, a następnie utwórzmy klasę IOCContainer (będącą naszym customowym kontenerem), wyglądającą w następujący sposób
public class IOCContainer
{
        private static readonly IKernel Kernel = new StandardKernel();

        protected IOCContainer()
        {

        }

        public static void Dispose()
        {
            Kernel.Dispose();
        }

        public static object Get(Type type)
        {
            return Kernel.Get(type);
        }

        public static object Get(Type type,string key)
        {
            return Kernel.Get(type, key);
        }
  
        public static IEnumerable<object>  GetAll(Type instanceType)
        {
            return Kernel.GetAll(instanceType);
        }
  
        public static void Bind<T, TS>() where TS : T
        {
            Kernel.Bind<T>().To<TS>();
        } 
    }
Następnie w klasie SilverlightBootstraper musimy przeciążyć funkcję Configure,GetAllInstances oraz GetInstance. Jako, że będziemy korzystali z naszego własnego kontenera w funkcji Configure rejestrujemy wszystkie potrzebne nam viewmodele oraz widoki. Zatem funkcja Configure powinna od teraz wyglądać tak
protected override void Configure()
        {
            IOCContainer.Bind<ShellView,ShellView>();
            IOCContainer.Bind<ShellViewModel,ShellViewModel>();
        }
Następnie musimy "pokazać" Caliburn-owi gdzie powinien szukać widoków. Dlatego też przeciążamy funkcje GetInstance oraz GetAllInstances i zmieniamy ich postać na następującą
protected override object GetInstance(Type serviceType, string key)
        {
            return IOCContainer.Get(serviceType, key);
        }

        protected override IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return IOCContainer.GetAll(serviceType);
        }
Od tej pory, za każdym razem gdy użyjemy mechanizmów Caliburna do bindowania viewmodelu z widokiem itp.,Caliburm będzie szukał widoków oraz viewmodeli w naszym kontenerze IOC.

2.2 Własna konwencja wyszukiwania widoków

W przedstawionym powyżej przykładzie bootstrappera, nasz bootstrapper korzystał z domyślnej konwencji rozwiązywania widoków na podstawie viewmodeli (do ShellViewModel został dopasowany widok ShellView). Czasem jednak konwencja ta, nie pasuje do konwencji przyjętej w danym projekcie. Sam biorę udział w projekcie, w którym widoki w kontenerze są rejestrowane następujący sposób
IOCContainer.Bind< IView<ViewModel>,View>
Czy zatem oznacza to, że nie mogę korzystać już mechanizmów Caliburna i musze zrezygnować chociażby z bootstrappera ?Oczywiście,że nie. Framework dostarcza nam możliwość zdefiniowania własnej konwencji wyszukiwania widoków na podstawie viewmodeli. W celu zastąpienia domyślnej konwencji musimy podpiąć się pod propercję LocateForModelType znajdującą się w klasie ViewLocator i zdefiniować własną funkcję wyszukującą widok na podstawie viewmodelu. W moim przypadku wygląda to w następujący sposób
ViewLocator.LocateForModelType = (modelType, displayLocation, context) =>
                                                 {

                                                     string viewTypeName = string.Format("{0}[[{1}]],{2}", typeof(IView<>).FullName, modelType.AssemblyQualifiedName, typeof(IView<>).Assembly.FullName);
                                                     Type viewType = Type.GetType(viewTypeName);
                                                     if (viewType == null)
                                                         throw new ArgumentNullException(string.Format("Nie odnaleziono typu widoku dla viewModelu"), modelType.FullName);

                                                     return ViewLocator.GetOrCreateViewType(viewType);
                                                 };
Funkcja ViewLocator.LocateForModelType odpalana jest za każdym razem, gdy CaliburnMicro chce wyszukać widok na podstawie viewmodleu. Najważniejszym argumentem funkcji LocateForModelType jest modelType - czyli typ viewmodelu, który posłuży nam do znalezienia widoku - a właściwie jego typu. Po odnalezieniu typu widoku, wywołujemy funkcję
ViewLocator.GetOrCreateViewType(viewType);
(które z kolei znajdzie nam w kontenerze pożądanym przez nas typ widoku) i zwracamy jej rezultat.

3. Bootstrapper - Windows Phone

W przypadku bootstrappera dla Windows Phona sytuacja wygląda odrobinę inaczej. W celu utworzenia własnego bootstrappera musimy rozszerzyć klasę PhoneBootstrapper. Utwórzmy zatem nowy projekt typu Windows Phone Application, oraz dodajmy do niego nową kontrolkę typu Windows Phone Portrait Page.
Podobnie jak w przypadku projektu Silverlightowego dodajemy referencję do CaliburnMicro ,dorzucamy klasę viewmodelu (którą nazywamy ShellViewModel) oraz dodajemy jakiegoś textboxa do ShellView. Następnie musimy utworzyć nasz bootstrapper, dodajmy zatem klasę WindowsPhoneBootstrapper rozszerzającą klasę PhoneBootstrapper. Podobnie jak w przypadku bootstrappera silverlightowego przeciążamy funkcje odpowiedzialne za konfigurację kontenera IOC.
public class WindowsPhoneBootstrapper : PhoneBootstrapper
    {
        protected override void Configure()
        {
          
           IOCContainer.BindToConstant<INavigationService>(new FrameAdapter(RootFrame));
           IOCContainer.BindToConstant<IPhoneService>(new PhoneApplicationServiceAdapter(RootFrame));
           IOCContainer.BindAsSingleton<ShellViewModel, ShellViewModel>();
        }

      
        protected override void OnExit(object sender, EventArgs e)
        {
            IOCContainer.Dispose();
            base.OnExit(sender, e);
        }

        protected override object GetInstance(Type serviceType, string key)
        {
            return IOCContainer.Get(serviceType, key);
        }

        protected override IEnumerable<object> GetAllInstances(Type serviceType)
        {
            return IOCContainer.GetAll(serviceType);
        }

        protected override void BuildUp(object instance)
        {
            IOCContainer.Inject(instance);
        }
    }
Zwróćmy uwagę, że w kontenerze IOC zostały zarejestrowane usługi nawigacji Windows Phona, do których został przekazany RootFrame. RootFrame jest to główne okno aplikacji, zostaje ono utworzone przez CliburnMicro. Tak samo jak w przypadku projektu Silverlightowego w pliku App.xaml.cs zostawiamy jedynie konstruktor z wywołaniem funkcji InitializeComponents(). W celu ustawienia ShellView jako RootFram-a musimy zmodyfikować plik WMAppManifest.xml (znajdujący się w katalogu Properties). Odnajdujemy tam wpis
<DefaultTask  Name ="_default" NavigationPage="MainPage.xaml"/>
i zamieniamy go na
<DefaultTask  Name ="_default" NavigationPage="ShellView.xaml"/>
Jak widać WindowsPhone wymusza tutaj podejście ViewFirst. Przy uruchomieniu aplikacji mechanizm nawigacji WindowsPhona odpali widok ShellView (bez wiedzy Caliburn'a), natomiast CaliburnMicro dopasuje odpowiedni ViewModel do naszego widoku - na podstawie domyślnej konwencji. Jeżeli chcielibyśmy zmodyfikować sposób wyszukiwania ViewModeli do widoków musimy podpiąć się do propertisa LocateForViewType znajdującego się w klasie ViewModelLocator
 ViewModelLocator.LocateForViewType = type =>
                                                     {
                                                         return //tutaj zwroc odpowiedni viewmodel na podstawie parametru type ;
                                                     };
W ostatnim kroku musimy jeszcze tylko dodać nasz bootstrapper do zasobów aplikacji
<Application.Resources>
        <Bootstrapper:WindowsPhoneBootstrapper x:Key="Bootstrapper"></Bootstrapper:WindowsPhoneBootstrapper>
    </Application.Resources>
odpalając teraz program naszym oczom powinien ukazać się taki oto widok

niedziela, 15 kwietnia 2012

Jak z kilku dllek zrobić jedną, czyli modularność przy pomocy ILMerge

Wstęp

Pracuję aktualnie nad strukturą pewnego projektu. Ideą, która przyświeca przy jej tworzeniu jest to, żeby była jak najbardziej modularna - tak by składała się z niezależnych, niepowiązanych ze sobą podaplikacji. 
Aby lepiej przybliżyć problem przyjrzyjmy się przykładowi:

Rysunek 1 - Przykładowa struktura
Przyjmijmy, że chcemy stworzyć moduł zarządzania użytkownikami, który będziemy używać w kilku tworzonych przez nas aplikacjach. Nie chcemy, bowiem za każdym razem wynajdywać koła od początku.  Moduł ten składać się będzie z podmodułów odpowiadających za rejestrację, logowanie, uprawnienia. 
Standardowo do każdego z tych "klocków" utworzylibyśmy osobny projekt, chcąc dodać do naszych aplikacji musielibyśmy wrzucić 4 osobne dllki:
- główną - Modułu Zarządzania Użytkownikami
- 3 zależne - Moduł Rejestracji, Moduł Logowania, Moduł Uprawnień
Dorzucenie 4 dllek nie wydaje się chyba wielkim problemem, prawda? 

Pójdźmy jednak dalej. Załóżmy, że nasza firma zajmuje się tworzeniem stron internetowych. Mamy kilka "żyjących" i rozwijanych stron. Każdą z nich zajmuje się osobny zespół programistów, mamy również dział geeków, którzy zajmuje się core'em. Nasi klienci stwierdzili, że chcieliby mieć możliwość logowania się i rejestracji przy pomocy otwartych systemów uwierzytelniania jak OpenID, Google, Facebook. Zespół naszych geeków czym prędzej zaczyna się tym zajmować i na koniec generuje następującą strukturę:

Rysunek 2 - Rozszerzona struktura

Przy uaktualnianiu Modułu Zarządzania Użytkownikami musimy pamiętać, że musimy dodać kolejne dziewięć dllek dla każdego z podmodułów. W sumie mamy trzynaście dllek, może i pechowa liczba, ale to dalej nie jest krytyczny problem, czyż nie? Krytycznie jednak zaczyna się robić, gdy mamy kilka używanych w wielu miejscach modułów, z których każdy składa się z podmodułów. Gdy jeszcze trzymamy się zasadom luźno powiązanych klas, inversion of control i korzystamy z kontenerów dependency injection to sytuacja robi się jeszcze bardziej problematyczna, a zapomnienie dorzucenia dllki łatwiejsze i bardziej problematyczne.

Dużym uproszczeniem byłaby sytuacja gdy każdy moduł jest osobną dllką. Dzięki temu programiści, którzy z niego korzystają nie muszą się zastanawiać z czego on dokładnie się składa, musiał by tylko wiedzieć jak się go używa. 

Co to jest ILMerge?

ILMerge jest narzędziem dostarczonym przez Microsoft, pozwalającym na łączenie kilku asemblatów w jeden (stąd jego nazwa - łączenie ILa). Radzi on sobie bez większych problemów również z łączeniem plików .pdb umożliwiając tym samym debugowanie. Dostarczany jest w postaci pliku EXE (można go pobrać pod linkiem), który uruchomiony z odpowiednimi parametrami pozwala nam na złączenie asemblatów. Przykładowe jego wywołanie to:

ilmerge /target:winexe /out:myprogram.exe yourexe.exe yourlibrary.dll

Gdzie:
- ilmerge - nazwa pliku ilmerge'a 
- /target - parametr mówiący czy nasz asemblat ma być plikiem exe (winexe) cz dllką (module
- /out: - parametr mówiący o nazwie wynikowego pliku, podajemy też asemblaty, które chcemy złączyć


Jak to zobaczyłem to stwierdziłem, że bardzo to fajne, tylko że wywoływanie za każdym razem polecenia z linii komend. Na szczęście znalazłem świetny artykuł Scotta Hanselmana jak można ten proces zautomatyzować.


Automatyczne wywołanie ILMerge


MSBuild pozwala na zdefiniowanie akcji, które będą wykonywane po procesie zbudowania projektu (tzw. Post build actions). Możemy zatem zdefiniować akcję, która będzie polegała na wywołaniu ILMerge'a z odpowiednimi parametrami dla wybranych przez nas projektów. Jak tego dokonać? Ponieważ pliki projektów są zarazem plikami MSBuilda możemy je odpowiednio zmodyfikować.
Zacznijmy od utworzenia nowej solucji i struktury projektów. Niech wygląda ona następująco:

Rysunek 3 - przykładowa struktura projektów

Odpowiada ona przykładowej strukturze projektów przedstawionej wcześniej. Mamy projekt ILMergeSample.UserManagementModule, który ma referencję do trzech podmodułów. Zbudujmy teraz nasz solucję i przejdźmy do katalogu Debug dla naszego projektu modułu obsługi pracowników.

Rysunek 4 - Wynik standardowego builda

Zgodnie z przewidywaniami standardowo skopiował dllki podmodułów. Przejdźmy więc do sedna artyukułu i zacznijmy łączyć je w jedną. Rozpocznijmy od skopiowania pliku ILMerge.exe do struktury naszej solucji (domyślnie znajduje się w "C:\Program Files (x86)\Microsoft\ILMerge"). Pozwoli nam to uniezależnić od tego czy inny developer ma go na swoim komputerze i pod jaką ścieżką się u niego znajduje.
Dodajmy również plik o nazwie "Ilmerge.CSharp.targets".
Nasza struktura solucji powinna wyglądać teraz następująco:


Rysunek 5 - Struktura projektu po dodaniu ILMerge'a


Otwórzmy teraz plik "Ilmerge.CSharp.targets" i wklejmy do niego następujące dane:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />   
 <Target Name="AfterBuild">     
 <CreateItem Include="@(ReferencePath)" Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'">
    
 
 <Message Text="MERGING: @(IlmergeAssemblies->'%(Filename)')" Importance="High" /> 
 
 <Exec Command="&quot;$(SolutionDir)\Ilmerge.exe&quot;
    /targetplatform:v4,&quot;%ProgramFiles%\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0&quot; 
    /out:@(MainAssembly) &quot;@(IntermediateAssembly)&quot; @(IlmergeAssemblies->'&quot;%(FullPath)&quot;', ' ')" /> 
 </Target> 
 
 <Target Name="_CopyFilesMarkedCopyLocal"/>     
</Project>

Co to tak naprawdę robi? Prześledźmy to po kolei:
- Project DefaultTargets="Build" - określamy tutaj, że definiujemy akcję dla builda
- Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" - importujemy tutaj domyślne ustawienia builda, nie chcemy bowiem wszystko definiować od początku, tylko nadpisać część ustawień
- CreateItem Include="@(ReferencePath)" Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'" - dzięki temu warunkowi wybieramy do złączenia te asemblaty, które są dołączone do naszego projektu i mają zaznaczoną opcję "Copy Local" oraz "ILMerge" na true
- Message Text - tutaj definiujemy wiadomość, która będzie informowała nas w outputcie o tym, że dokonujemy złączenia asemblatów w trakcie budowania projektu
- Exec Command - tutaj definiujemy odpowiednie wywołanie ILMerge'a. Moja konfiguracja jest specyficzna dla .NET 4.0, jeżeli macie asemblaty w innej wersji .NET powinniście zmodyfikować parametr /targetplatform


Mając konfigurację MSBuilda z ILMerge powinniśmy jeszcze poinformować nasz projekt, że ma z niej korzystać. Dokonujemy tego poprzez ręczną modyfikację pliku projektu (naciskamy na niego prawym przyciskiem i wybieramy opcję "Edit Project File").
Po otworzeniu pliku projektu naszego Modułu Zarządzania Użytkownikami powinniśmy odnaleźć następujące linijki:


Rysunek 6 - Plik projektu przed modyfikacjami


Jedyne co musimy zrobić to zmodyfikować plik następująco:


Rysunek 7 - Plik projektu po modyfikacjach


Dodaliśmy tylko dla wybranych przez nas projektów zmienną <IlMerger> z wartością true informując o tym, że chcemy, żeby projekt został połączony i podmieniliśmy domyślną konfigurację builda przygotowaną wcześniej w pliku "Ilmerge.CSharp.targets".

Zapiszmy teraz plik projektu, przeładujmy go i przebudujmy. Ponownie zerknijmy do katalogu Debug naszego Modułu Zarządzania Użytkownikami
i ujrzymy, że została wygenerowana tylko jedna dllka.


Rysynek 8 - Wygenerowane pliki po konfiguracji ILMerge


Czy ILMerge działa dla Silverlighta i Phone'a?


Ależ owsze, czemu nie. Należy tylko odpowiednio spreparować nasz plik targets:
- dla Phone'a nasz plik wyglądał by następująco:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">    
  <Import Project="$(MSBuildExtensionsPath)\Microsoft\Silverlight for Phone\$(TargetFrameworkVersion)\Microsoft.Silverlight.$(TargetFrameworkProfile).Overrides.targets" />
  <Import Project="$(MSBuildExtensionsPath)\Microsoft\Silverlight for Phone\$(TargetFrameworkVersion)\Microsoft.Silverlight.CSharp.targets" />
   
 <Target Name="AfterBuild">     
 <CreateItem Include="@(ReferencePath)" Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'">
    
 
 <Message Text="MERGING: @(IlmergeAssemblies->'%(Filename)')" Importance="High" /> 
 
 <Exec Command="&quot;$(SolutionDir)\Ilmerge.exe&quot;
    /targetplatform:v4,&quot;%ProgramFiles%\Reference Assemblies\Microsoft\Framework\Silverlight\v4.0\Profile\WindowsPhone&quot; 
    /out:@(MainAssembly) &quot;@(IntermediateAssembly)&quot; @(IlmergeAssemblies->'&quot;%(FullPath)&quot;', ' ')" /> 
 </Target> 
 
 <Target Name="_CopyFilesMarkedCopyLocal"/>     
</Project>
- dla Silverlight'a nasz plik wyglądał by następująco:
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">  
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\Silverlight\$(SilverlightVersion)\Microsoft.Silverlight.CSharp.targets" />   
 <Target Name="AfterBuild">     
 <CreateItem Include="@(ReferencePath)" Condition="'%(CopyLocal)'=='true' and '%(ReferencePath.IlMerge)'=='true'">
    
 
 <Message Text="MERGING: @(IlmergeAssemblies->'%(Filename)')" Importance="High" /> 
 
 <Exec Command="&quot;$(SolutionDir)\Ilmerge.exe&quot;
    /targetplatform:v4,&quot;%ProgramFiles%\Reference Assemblies\Microsoft\Framework\Silverlight\v5.0&quot; 
    /out:@(MainAssembly) &quot;@(IntermediateAssembly)&quot; @(IlmergeAssemblies->'&quot;%(FullPath)&quot;', ' ')" /> 
 </Target> 
 
 <Target Name="_CopyFilesMarkedCopyLocal"/>     
</Project>

Jak łatwo zauważyć podmieniliśmy tylko ścieżki do plików z domyślnymi ustawieniami buildów dla tych środowisk oraz poprawiliśmy wywołanie ILMerge'a w Exec Command tak aby dotyczyła właściwej platformy.


ILMerge i Resharper


Jeżeli zrobiliście wszystko zgodnie z powyższym opisem to po dodaniu klas i próbie wywołania ich w projekcie Application okaże się, że Resharper nie widzi klas z podmodułów (Authorization, Registration, Login). Dzieje się tak dlatego, że projekty naszych modułów znajdują się w tej samej solucji co projekt Application. Jeżeli utworzylibyśmy osobną solucję, w której nie znajdowałyby się nasze podmoduły, a tylko projekt Application okaże się, że Resharper widzi je już bez większych problemów.
Jest to błąd Resharpera - chociaż oni twierdzą inaczej. Zapewne nie chce im się tego naprawiać, ale oczywiście mają na to mądre wytłumaczenie. Mówią, że jeżeli doprowadziło się do takiej sytuacji to znaczy, że układ projektów jest zły. Można by się było z nimi po części zgodzić, bo moduły powinny być traktowane jako całość i posiadać osobne solucje a nie być wepchnięte w jedną dużą. Jest to jednak spore uniedogodnienie, szczególnie na początku tworzenia systemu, gdy moduły są często zmieniane - trzeba wtedy pracować na kilku solucjach na raz, albo ignorować błędnie podświetlony przez Resharpera kod.


Podsumowanie i linki


Mam nadzieję, że tym artykułem udało mi się przybliżyć to jak ILMerge może pomóc nam przy tworzeniu modularnych aplikacji oraz przede wszystkim ułatwić życie programistom.
Kody źródłowe przykładów z tego artykułu możecie znaleźć tutaj.

Linki do artykułów z których korzystałem przy tworzeniu tego wpisu to:
- http://www.hanselman.com/blog/MixingLanguagesInASingleAssemblyInVisualStudioSeamlesslyWithILMergeAndMSBuild.aspx
- http://blogs.msdn.com/b/jomo_fisher/archive/2006/03/05/544144.aspx
- http://blogs.clariusconsulting.net/kzu/leveraging-ilmerge-to-simplify-deployment-and-your-users-experience/
- http://awkwardcoder.blogspot.com/2011/05/using-ilmerge-for-windows-phone-7.html
- http://albao.wordpress.com/tag/ilmerge-error-documentation-exception-net/
- http://nitoprograms.blogspot.com/2010/09/using-ilmerge-with-net-40-andor-rx.html
- http://devnet.jetbrains.net/message/5253869#5253869

 
Design by Free WordPress Themes | Bloggerized by Lasantha - Premium Blogger Themes | Online Project management