2011-01-22 6 views
12

La pregunta es como suena. No importa cuán duro y con qué frecuencia trate de entender WPF, siento que me estoy golpeando la cabeza contra la pared. Me gusta Winforms, donde todo tiene sentido.¿Entrenar a un novato a través de WPF básico? (No lo grok).

Como ejemplo, estoy tratando de escribir una aplicación simple que me permita diseñar un conjunto de rutas bidimensionales (representadas por polilíneas) y arrastrar sus vértices, y tener la información de vértices sincronizada con un presentador (es decir, supongo, un modelo de vista)

Así que el problema es por lo tanto:

  • hacer que la ventana reconoce una instancia de IExtendedObjectPresenter como fuente de datos;
  • De una colección de IExtendedObject, dibuje una polilínea para cada IExtendedObject;
  • Para cada vértice en el objeto extendido, representado por la colección IExtendedObject.Points, coloque un vértice de polilínea en las coordenadas especificadas.

El IDE no me está ayudando en absoluto aquí. Ninguna de las muchas propiedades disponibles en XAML suenan de manera significativa para mí. Como parece que se hace mucho de manera implícita, no hay un lugar obvio para decirle a la ventana qué hacer.

Antes de programarme y contarle a RTFM, me gustaría volver a enfatizar que he investigado los conceptos básicos de WPF muchas veces. No sé nada más de lo que lo hice cuando se lanzó por primera vez. Parece totalmente inpenetrable. Los ejemplos dados para un tipo de comportamiento no son de ninguna manera aplicables a un tipo de comportamiento incluso ligeramente diferente, por lo que regresas al punto de partida. Espero que la repetición y los exámenes dirigidos enciendan una luz en mi cabeza en algún momento.

+0

@Tom Aconsejaría intentar algunas cosas en código en lugar de en Xaml si Xaml te está volviendo loco. –

+0

@Tom Sugiero comenzar con un control 'Canvas' y jugar con agregar elementos y cambiar sus posiciones programáticamente. Puede tener una idea de cómo trabajar con rutas, pero sin tener que lidiar con el head-boiler de Xaml al mismo tiempo. –

+0

Quien vote para cerrar, por favor explique por qué. Es una pregunta válida Gracias. –

Respuesta

26

Me solidarizo contigo. Realmente entender WPF lleva mucho tiempo y puede ser muy frustrante para lograr la más simple de las cosas. Pero meterse en un problema que no es fácil para los expertos solo es buscar problemas. Debe hacer frente a tareas más simples y leer un montón de código hasta que las cosas comiencen a tener sentido. Donald Knuth dice que realmente no conoces el material hasta que haces los ejercicios.

He resuelto tu problema y admito que hay muchos conceptos avanzados para hacer esto de forma limpia y virando en MVVM lo hace mucho más difícil. Por lo que vale, aquí hay una solución de código cero detrás de su problema que está en el espíritu de MVVM.

Aquí es el XAML:

<Grid> 
    <Grid.Resources> 
     <local:PolylineCollection x:Key="sampleData"> 
      <local:Polyline> 
       <local:Coordinate X="50" Y="50"/> 
       <local:Coordinate X="100" Y="100"/> 
       <local:Coordinate X="50" Y="150"/> 
      </local:Polyline> 
     </local:PolylineCollection> 
    </Grid.Resources> 
    <Grid DataContext="{StaticResource sampleData}"> 
     <ItemsControl ItemsSource="{Binding Segments}"> 
      <ItemsControl.ItemsPanel> 
       <ItemsPanelTemplate> 
        <Canvas/> 
       </ItemsPanelTemplate> 
      </ItemsControl.ItemsPanel> 
      <ItemsControl.ItemTemplate> 
       <DataTemplate> 
        <Line X1="{Binding Start.X}" Y1="{Binding Start.Y}" X2="{Binding End.X}" Y2="{Binding End.Y}" Stroke="Black" StrokeThickness="2"/> 
       </DataTemplate> 
      </ItemsControl.ItemTemplate> 
     </ItemsControl> 
     <ItemsControl ItemsSource="{Binding ControlPoints}"> 
      <ItemsControl.ItemsPanel> 
       <ItemsPanelTemplate> 
        <Canvas/> 
       </ItemsPanelTemplate> 
      </ItemsControl.ItemsPanel> 
      <ItemsControl.ItemContainerStyle> 
       <Style TargetType="ContentPresenter"> 
        <Setter Property="Canvas.Left" Value="{Binding X}"/> 
        <Setter Property="Canvas.Top" Value="{Binding Y}"/> 
       </Style> 
      </ItemsControl.ItemContainerStyle> 
      <ItemsControl.ItemTemplate> 
       <DataTemplate> 
        <Ellipse Margin="-10,-10,0,0" Width="20" Height="20" Stroke="DarkBlue" Fill="Transparent"> 
         <i:Interaction.Behaviors> 
          <local:ControlPointBehavior/> 
         </i:Interaction.Behaviors> 
        </Ellipse> 
       </DataTemplate> 
      </ItemsControl.ItemTemplate> 
     </ItemsControl> 
    </Grid> 
</Grid> 

y aquí están las clases de apoyo:

public class Coordinate : INotifyPropertyChanged 
{ 
    private double x; 
    private double y; 

    public double X 
    { 
     get { return x; } 
     set { x = value; OnPropertyChanged("X", "Point"); } 
    } 
    public double Y 
    { 
     get { return y; } 
     set { y = value; OnPropertyChanged("Y", "Point"); } 
    } 
    public Point Point 
    { 
     get { return new Point(x, y); } 
     set { x = value.X; y = value.Y; OnPropertyChanged("X", "Y", "Point"); } 
    } 

    public event PropertyChangedEventHandler PropertyChanged; 

    private void OnPropertyChanged(params string[] propertyNames) 
    { 
     foreach (var propertyName in propertyNames) 
      if (PropertyChanged != null) 
       PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); 
    } 
} 

public class Polyline : List<Coordinate> 
{ 
} 

public class Segment 
{ 
    public Coordinate Start { get; set; } 
    public Coordinate End { get; set; } 
} 

public class PolylineCollection : List<Polyline> 
{ 
    public IEnumerable<Segment> Segments 
    { 
     get 
     { 
      foreach (var polyline in this) 
      { 
       var last = polyline.FirstOrDefault(); 
       foreach (var coordinate in polyline.Skip(1)) 
       { 
        yield return new Segment { Start = last, End = coordinate }; 
        last = coordinate; 
       } 
      } 
     } 
    } 

    public IEnumerable<Coordinate> ControlPoints 
    { 
     get 
     { 
      foreach (var polyline in this) 
      { 
       foreach (var coordinate in polyline) 
        yield return coordinate; 
      } 
     } 
    } 
} 

public class ControlPointBehavior : Behavior<FrameworkElement> 
{ 
    private bool mouseDown; 
    private Vector delta; 

    protected override void OnAttached() 
    { 
     var canvas = AssociatedObject.Parent as Canvas; 
     AssociatedObject.MouseLeftButtonDown += (s, e) => 
     { 
      mouseDown = true; 
      var mousePosition = e.GetPosition(canvas); 
      var elementPosition = (AssociatedObject.DataContext as Coordinate).Point; 
      delta = elementPosition - mousePosition; 
      AssociatedObject.CaptureMouse(); 
     }; 
     AssociatedObject.MouseMove += (s, e) => 
     { 
      if (!mouseDown) return; 
      var mousePosition = e.GetPosition(canvas); 
      var elementPosition = mousePosition + delta; 
      (AssociatedObject.DataContext as Coordinate).Point = elementPosition; 
     }; 
     AssociatedObject.MouseLeftButtonUp += (s, e) => 
     { 
      mouseDown = false; 
      AssociatedObject.ReleaseMouseCapture(); 
     }; 
    } 
} 

Esta solución utiliza comportamientos, que son ideales para la implementación de la interactividad con MVVM.

Si no está familiarizado con los comportamientos, instale el Expression Blend 4 SDK y añadir a espacios de nombres:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 

y añadir System.Windows.Interactivity a su proyecto.

+2

Gracias, este es un ejemplo fantástico y muy detallado. Se toma mucho tiempo para publicarlo y es muy apreciado. He dicho lo mismo a ** vorrtex ** porque ambos son muy útiles. –

+1

@Rick ¡Desearía poder votar dos veces! Gracias por tomarse el tiempo para explicar este concepto y brindar una respuesta detallada. Noooooow lo entiendo – dFlat

+0

Wow. Si bien estoy de acuerdo con que esta no es una pregunta adecuada para StackOverflow ya que está fuera de tema> demasiado amplia, realmente tengo que decir que tu respuesta fue suficiente. Genial, hombre. Gracias. Funciona de la caja y es útil. – anhoppe

13

Mostraré cómo crear una aplicación WPF con patrón MVVM para 2D-Poliline con vértices arrastrables.

PointViewModel.cs

public class PointViewModel: ViewModelBase 
{ 
    public PointViewModel(double x, double y) 
    { 
     this.Point = new Point(x, y); 
    } 

    private Point point; 

    public Point Point 
    { 
     get { return point; } 
     set 
     { 
      point = value; 
      OnPropertyChanged("Point"); 
     } 
    } 
} 

La clase ViewModelBase incluye solamente una implementación de la interfaz INotifyPropertyChanged. Esto es necesario para reflejar los cambios de la propiedad clr en la representación visual.

LineViewModel.cs

public class LineViewModel 
{ 
    public LineViewModel(PointViewModel start, PointViewModel end) 
    { 
     this.StartPoint = start; 
     this.EndPoint = end; 
    } 

    public PointViewModel StartPoint { get; set; } 
    public PointViewModel EndPoint { get; set; } 
} 

Tiene referencias a puntos, por lo que los cambios serán recibidas automáticamente.

MainViewModel.cs

public class MainViewModel 
{ 
    public MainViewModel() 
    { 
     this.Points = new List<PointViewModel> 
     { 
      new PointViewModel(30, 30), 
      new PointViewModel(60, 100), 
      new PointViewModel(50, 120) 
     }; 
     this.Lines = this.Points.Zip(this.Points.Skip(1).Concat(this.Points.Take(1)), 
      (p1, p2) => new LineViewModel(p1, p2)).ToList(); 
    } 

    public List<PointViewModel> Points { get; set; } 
    public List<LineViewModel> Lines { get; set; } 
} 

Contiene una muestra de datos de puntos y líneas

MainVindow.xaml

<Window.Resources> 
    <ItemsPanelTemplate x:Key="CanvasPanelTemplate"> 
     <Canvas/> 
    </ItemsPanelTemplate> 
    <Style x:Key="PointListBoxItem"> 
     <Setter Property="Canvas.Left" Value="{Binding Point.X}"/> 
     <Setter Property="Canvas.Top" Value="{Binding Point.Y}"/> 
    </Style> 
    <DataTemplate x:Key="LineTemplate"> 
     <Line X1="{Binding StartPoint.Point.X}" X2="{Binding EndPoint.Point.X}" Y1="{Binding StartPoint.Point.Y}" Y2="{Binding EndPoint.Point.Y}" Stroke="Blue"/> 
    </DataTemplate> 
    <DataTemplate x:Key="PointTemplate"> 
     <view:PointView /> 
    </DataTemplate> 
</Window.Resources> 
<Grid> 
    <ItemsControl ItemsSource="{Binding Lines}" ItemsPanel="{StaticResource CanvasPanelTemplate}" ItemTemplate="{StaticResource LineTemplate}"/> 
    <ItemsControl ItemsSource="{Binding Points}" ItemContainerStyle="{StaticResource PointListBoxItem}" ItemsPanel="{StaticResource CanvasPanelTemplate}" 
        ItemTemplate="{StaticResource PointTemplate}"/> 
</Grid> 

Aquí hay una gran cantidad de trucos. En primer lugar, estos ItemsControls se basan no en el StackPanel vertical, sino en el Canvas.El ItemsControl de puntos aplica una plantilla de contenedor especial con el objetivo de colocar elementos en las coordenadas necesarias. Pero ItemsControl de líneas no requieren tales plantillas, y es extraño en algún momento. Dos últimas DataTemplates son obvias.

PointView.xaml

<Ellipse Width="12" Height="12" Stroke="Red" Margin="-6,-6,0,0" Fill="Transparent"/> 

márgenes izquierdo y superior son iguales a la mitad de la Width y la Height. Tenemos un Fill transparente porque esta propiedad no tiene un valor predeterminado y los eventos del mouse no funcionan.

Eso es casi todo. Solo queda la funcionalidad de arrastrar y soltar.

PointView.xaml.cs

public partial class PointView : UserControl 
{ 
    public PointView() 
    { 
     InitializeComponent(); 

     this.MouseLeftButtonDown += DragSource_MouseLeftButtonDown; 
     this.MouseMove += DragSource_MouseMove; 
    } 

    private bool isDraggingStarted; 

    private void DragSource_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) 
    { 
     this.isDraggingStarted = true; 
    } 

    private void DragSource_MouseMove(object sender, MouseEventArgs e) 
    { 
     if (isDraggingStarted == true) 
     { 
      var vm = this.DataContext as PointViewModel; 
      var oldPoint = vm.Point; 

      DataObject data = new DataObject("Point", this.DataContext); 
      DragDropEffects effects = DragDrop.DoDragDrop(this, data, DragDropEffects.Move); 

      if (effects == DragDropEffects.None) //Drag cancelled 
       vm.Point = oldPoint; 

      this.isDraggingStarted = false; 
     } 
    } 

MainVindow.xaml.cs

public partial class MainWindow : Window 
{ 
    public MainWindow() 
    { 
     InitializeComponent(); 
     this.DataContext = new MainViewModel(); 

     this.AllowDrop = true; 
     this.DragOver += DropTarget_DragOver; 

    } 

    private void DropTarget_DragOver(object sender, DragEventArgs e) 
    { 
     var vm = e.Data.GetData("Point") as PointViewModel; 
     if (vm != null) 
      vm.Point = e.GetPosition(this); 
    } 
} 

Así que la muestra se realiza mediante 2 archivos XAML y 3 ViewModels.

+0

Gracias, este es un ejemplo fantástico y muy detallado. Se toma mucho tiempo para publicarlo y es muy apreciado. He dicho lo mismo a ** Rick Sladkey ** porque ambos son muy útiles. –

Cuestiones relacionadas