.NET 3.0 en liten översikt - del 5 (WPF)
Förord
Microsoft har nyligen släppt version 3.0 av .NET frameworket. Vad innehåller det för nytt och användbart då? Vi ska granska en del av nyheterna i en liten artikelserie. Vi fortsätter att titta på Windows Presentation Foundation (WPF).Innehåll
»»
»
»
»
»
»
Relaterade artiklar
» .NET 3.0 en liten översikt - del 1» .NET 3.0 en liten översikt - del 2
» .NET 3.0 en liten översikt - del 3
» .NET 3.0 en liten översikt - del 4 (WPF)
» .NET 3.0 en liten översikt - del 6 (WPF) - sista delen
Vi diskuterade WPF allmänt i förra artikeln och doppade tårna lite grann genom att kolla igenom entrypointen App.xaml samt dess codebehind fil.
Vi fortsätter med med första sidan..
Vi låter huvudfönstret vara ett NavigationWindow, som tillhandahåller funktionalitet att navigera runt. Vanligaste ändamål för detta är kanske nån sorts wizard (om du inte vill göra din egna webbläsare då alltså). Men funkar finfint även för våra ändamål. I vanliga fall visas en navigeringslist med back och forward knappar. Dessa har vi inget behov av här så därför gömmer vi dom med hjälp av ShowsNavigationUI="False".
Inte heller här är codebehind filen speciellt upphetsande:
Ett sätt som rekommenderas av dom som vet är att använda sej av nåt som kallas DataModel, View, ViewModel (alternativt Model, View, ViewModel) Detta liknar lite Model View Controller arkitekturen, men ViewModel är alltså ett lager som så att säga wrappar affärslogik relevant för en viss view på lämpligt sätt. Dan Crevier har skrivit en verklighetsnära artikelserie om just detta. John Gossman skriver om M, V, VM
Vi gör en klass som enkapsulerar kommunikationen med servern och som vi senare använder för databindning. I brist på bättre namn har jag kallat denna "Remote"
Using statements och variabel deklarationer. Vi vill också att callback beteendet skall vara reentrant och vi sköter trådsynkroniseringen själv.
Konstruktor, upprätta kontakt till servern (jo, felhanteringen saknas...) och spara undan ett Dispatcher objekt för trådsynkronisering.
Egenskaper relevanta för databindning.
Kod för kommunikation med servern.
Gränssnittet INotifyPropertyChanged är viktigt, av den anledningen att då fattar WPF att uppdatera databindningen när nåt ändrat. För collections finns INotifyCollectionChanged, fast här är kanske vanligare att direkt använda sej av ObservableCollection<> som vi senare kommer att se.
MessageCollection är en klass som ärver ObservableCollection<>, och som vi därför kan databinda på ett bra sätt.
Nu har vi nästan gått igenom hela vår ViewModel förutom två grejer relaterade till:
För att vi skall kunna visa meddelandets tidsstämpel som en analog klocka behöver vi en konverterare mellan DateTime och nånting lämpligt. Jag har här valt att konvertera till GeometryDrawing. GeometryDrawing är alltså geometriska figurer som kan användas som "data source" för en bild. En konverterare görs genom att implementera gränssnittet IValueConverter på lämpligt sätt.
Hur vi sedan använder denna konverterare ser vi i nästa artikel.
Jag har valt att implementera fula ord detektionen på samma sätt med hjälp av en konverterare. I den här konverteraren tar vi parameter objektet i användning för att slippa hårdkoda de fula orden i konverteraren. En annan möjlighet skulle ha varit att använda valideringsregler genom att skriva en egen ValidationRule. Eventuellt gör jag en sådan version i en senare artikel.
Detta blev en lång artikel. Egentligen är det bara Connect.xaml och ChatPage.xaml som vi har kvar att diskutera. Det gör vi i nästa del av artikelserien.
Som vanligt, frågor och kommentarer mottages tacksamt!
Vi fortsätter med med första sidan..
MainForm.xaml
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Chat client" Height="300" Width="300"
Source="Connect.xaml" ShowsNavigationUI="False">
Vi låter huvudfönstret vara ett NavigationWindow, som tillhandahåller funktionalitet att navigera runt. Vanligaste ändamål för detta är kanske nån sorts wizard (om du inte vill göra din egna webbläsare då alltså). Men funkar finfint även för våra ändamål. I vanliga fall visas en navigeringslist med back och forward knappar. Dessa har vi inget behov av här så därför gömmer vi dom med hjälp av ShowsNavigationUI="False".
Inte heller här är codebehind filen speciellt upphetsande:
using System;
namespace LemonDesign.Chat.Client {
///
/// Interaction logic for MainForm.xaml
///
public partial class MainForm : System.Windows.Navigation.NavigationWindow {
public MainForm() {
InitializeComponent();
}
}
}
DataModel, View, ViewModel
Ett sätt som rekommenderas av dom som vet är att använda sej av nåt som kallas DataModel, View, ViewModel (alternativt Model, View, ViewModel) Detta liknar lite Model View Controller arkitekturen, men ViewModel är alltså ett lager som så att säga wrappar affärslogik relevant för en viss view på lämpligt sätt. Dan Crevier
Remote
Vi gör en klass som enkapsulerar kommunikationen med servern och som vi senare använder för databindning. I brist på bättre namn har jag kallat denna "Remote"Using statements och variabel deklarationer. Vi vill också att callback beteendet skall vara reentrant och vi sköter trådsynkroniseringen själv.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.ServiceModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using LemonDesign.Chat.Contract;
namespace LemonDesign.Chat.Client.ViewModel {
/// Create a sort of viewmodel of the chat service
///
/// The callback behavior thing is important, otherwise we'll
/// end up with deadlock sooner or later
[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant, UseSynchronizationContext = false)]
public class Remote : IChatEvents, INotifyPropertyChanged {
private DuplexChannelFactory m_factory;
private IChat m_server;
private MessageCollection m_messages;
private bool m_isConnected;
private Dispatcher m_dispatcher;
Konstruktor, upprätta kontakt till servern (jo, felhanteringen saknas...) och spara undan ett Dispatcher objekt för trådsynkronisering.
///
/// Create a new instance
///
/// the url to connect to
/// the username
public Remote(string url, string user) {
NetNamedPipeBinding binding = new NetNamedPipeBinding();
m_factory = new DuplexChannelFactory(new InstanceContext(this), binding);
EndpointAddress address = new EndpointAddress(url);
m_server = m_factory.CreateChannel(address);
m_messages = new MessageCollection();
m_server.Connect(user);
IsConnected = true;
/* dispatcher, think Control.Invoke from WinForms land
* used to get things back on the UI thread */
m_dispatcher = Dispatcher.CurrentDispatcher;
}
Egenskaper relevanta för databindning.
///
/// All messages received from server, used in databinding
///
public MessageCollection Messages {
get { return m_messages; }
}
///
/// Indicate connection status
///
public bool IsConnected {
get { return m_isConnected; }
set {
if (value != m_isConnected) {
m_isConnected = value;
OnPropertyChanged("IsConnected");
}
}
}
Kod för kommunikation med servern.
///
/// Send message to server
///
/// the message to send
internal void Send(string message) {
try {
m_server.Say(message);
} catch (FaultException fault) {
MessageBox.Show(fault.Message);
}
}
#region IChatEvents Members
///
/// Server notifies that it is going down
///
public void ServerGoingDown() {
IsConnected = false;
}
///
/// Server received a message
///
/// the message
public void MessageReceived(Message message) {
// ObservableCollection<> et al. only allows
// manipulations from UI thread, so go there...
m_dispatcher.BeginInvoke(DispatcherPriority.Background,
(Action)delegate(Message m) {
m_messages.Insert(0, m);
Debug.Print("Adding {0}", m);
}, message);
}
#endregion
INotifyPropertyChanged
Gränssnittet INotifyPropertyChanged är viktigt, av den anledningen att då fattar WPF att uppdatera databindningen när nåt ändrat. För collections finns INotifyCollectionChanged, fast här är kanske vanligare att direkt använda sej av ObservableCollection<> som vi senare kommer att se.
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string property) {
PropertyChangedEventHandler hander = PropertyChanged;
if (hander != null) {
hander(this, new PropertyChangedEventArgs(property));
}
}
#endregion
MessageCollection
MessageCollection är en klass som ärver ObservableCollection<>, och som vi därför kan databinda på ett bra sätt.
using System;
using System.Collections.ObjectModel;
using LemonDesign.Chat.Contract;
namespace LemonDesign.Chat.Client.ViewModel {
///
/// Message collection for data binding purposes
///
public class MessageCollection : ObservableCollection {
}
}
Nu har vi nästan gått igenom hela vår ViewModel förutom två grejer relaterade till:
- Klockan
- Datatriggern för fula ord
Klockan
För att vi skall kunna visa meddelandets tidsstämpel som en analog klocka behöver vi en konverterare mellan DateTime och nånting lämpligt. Jag har här valt att konvertera till GeometryDrawing. GeometryDrawing är alltså geometriska figurer som kan användas som "data source" för en bild. En konverterare görs genom att implementera gränssnittet IValueConverter på lämpligt sätt.
using System;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows;
using System.Diagnostics;
namespace LemonDesign.Chat.Client.ViewModel {
[ValueConversion(typeof(DateTime), typeof(GeometryDrawing))]
public class DateTimeToGeometryDrawing : IValueConverter{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
DateTime timestamp = ((DateTime)value).ToLocalTime();
TimeSpan span = timestamp - DateTime.Today;
GeometryGroup group = new GeometryGroup();
group.Children.Add(new EllipseGeometry(new Rect(-10, -10, 20, 20)));
// hour hand
group.Children.Add(new LineGeometry(new Point(0, 0), new Point(0, -5),
new RotateTransform(span.TotalMinutes / (60 * 12.0) * 360)));
group.Children.Add(new LineGeometry(new Point(0, 0), new Point(0, -10),
new RotateTransform(timestamp.Minute / 60.0 * 360)));
return new GeometryDrawing(Brushes.Transparent, new Pen(Brushes.BlueViolet, 2), group);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
}
Hur vi sedan använder denna konverterare ser vi i nästa artikel.
ObscenityPredicate
Jag har valt att implementera fula ord detektionen på samma sätt med hjälp av en konverterare. I den här konverteraren tar vi parameter objektet i användning för att slippa hårdkoda de fula orden i konverteraren. En annan möjlighet skulle ha varit att använda valideringsregler genom att skriva en egen ValidationRule. Eventuellt gör jag en sådan version i en senare artikel.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Data;
namespace LemonDesign.Chat.Client.ViewModel {
[ValueConversion(typeof(string), typeof(bool))]
public class ObscenityPredicate : IValueConverter{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
string message = value as string;
if (message == null) return false;
message = message.ToLower();
string[] filterWords = ((string)parameter).Split('|');
foreach (string badWord in filterWords) {
foreach (string word in message.Split(' ')) {
if (badWord.ToLower().Trim() == word) {
return true;
}
}
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
throw new Exception("The method or operation is not implemented.");
}
#endregion
}
}
Detta blev en lång artikel. Egentligen är det bara Connect.xaml och ChatPage.xaml som vi har kvar att diskutera. Det gör vi i nästa del av artikelserien.
Som vanligt, frågor och kommentarer mottages tacksamt!
0 Kommentarer