Jak uzyskać projektanta formularzy systemu Windows w programie Visual Studio 2008, aby renderował formularz, który implementuje abstrakcyjną klasę bazową?

98

Zaangażowałem się w problem z dziedziczonymi kontrolkami w Windows Forms i potrzebuję porady na ten temat.

Używam klasy bazowej dla elementów na liście (własna lista GUI utworzona z panelu) i niektórych dziedziczonych kontrolek, które są dla każdego typu danych, które można dodać do listy.

Nie było z tym problemu, ale teraz odkryłem, że dobrze byłoby uczynić podstawową kontrolkę klasą abstrakcyjną, ponieważ ma ona metody, które muszą być zaimplementowane we wszystkich dziedziczonych kontrolkach, wywoływanych z kodu wewnątrz kontrola podstawowa, ale nie może i nie może być implementowana w klasie bazowej.

Kiedy oznaczam formant podstawowy jako abstrakcyjny, projektant Visual Studio 2008 odmawia załadowania okna.

Czy istnieje sposób, aby projektant pracował z abstrakcyjną kontrolką bazową?

Oliver Friedrich
źródło

Odpowiedzi:

97

WIEDZIAŁEM, że musi być sposób, aby to zrobić (i znalazłem sposób, aby to zrobić czysto). Rozwiązanie Shenga jest dokładnie tym, co wymyśliłem jako tymczasowe obejście, ale po tym, jak znajomy wskazał, że Formklasa ostatecznie odziedziczyła po abstractklasie, POWINIENEMY być w stanie to zrobić. Jeśli mogą to zrobić, możemy to zrobić.

Przeszliśmy od tego kodu do problemu

Form1 : Form

Problem

public class Form1 : BaseForm
...
public abstract class BaseForm : Form

W tym miejscu pojawiło się pierwsze pytanie. Jak wspomniano wcześniej, znajomy wskazał, że System.Windows.Forms.Formimplementuje abstrakcyjną klasę bazową. Udało nam się znaleźć ...

Dowód lepszego rozwiązania

Dzięki temu wiedzieliśmy, że projektant mógł pokazać klasę, która zaimplementowała podstawową klasę abstrakcyjną, po prostu nie mógł pokazać klasy projektanta, która natychmiast zaimplementowała podstawową klasę abstrakcyjną. Musiało być maksymalnie 5 między nimi, ale przetestowaliśmy 1 warstwę abstrakcji i początkowo wymyśliliśmy to rozwiązanie.

Wstępne rozwiązanie

public class Form1 : MiddleClass
...
public class MiddleClass : BaseForm
... 
public abstract class BaseForm : Form
... 

To faktycznie działa i projektant czyni to dobrze, problem rozwiązany… z wyjątkiem tego, że masz dodatkowy poziom dziedziczenia w swojej aplikacji produkcyjnej, który był konieczny tylko z powodu nieadekwatności w projektancie winforms!

To nie jest w 100% pewne rozwiązanie, ale jest całkiem dobre. Zasadniczo używasz #if DEBUGdo wymyślania wyrafinowanego rozwiązania.

Dopracowane rozwiązanie

Form1.cs

#if DEBUG
public class Form1 : MiddleClass
#else 
public class Form1 : BaseForm
#endif
...

MiddleClass.cs

public class MiddleClass : BaseForm
... 

BaseForm.cs

public abstract class BaseForm : Form
... 

To, co to robi, to użycie rozwiązania opisanego w „rozwiązaniu początkowym”, tylko jeśli jest w trybie debugowania. Chodzi o to, że nigdy nie wydasz trybu produkcyjnego poprzez kompilację do debugowania i że zawsze będziesz projektować w trybie debugowania.

Projektant zawsze będzie działał na podstawie kodu zbudowanego w bieżącym trybie, więc nie można używać projektanta w trybie wydania. Jednak jeśli projektujesz w trybie debugowania i publikujesz kod zbudowany w trybie wydania, możesz działać.

Jedynym pewnym rozwiązaniem byłoby przetestowanie trybu projektowania za pomocą dyrektywy preprocesora.

smelch
źródło
3
Czy Twój formularz i abstrakcyjna klasa bazowa mają konstruktor bez argonu? Bo to wszystko, co musieliśmy dodać, aby projektant pracował nad pokazaniem formy, która odziedziczyła po abstrakcyjnej formie.
nr
Działało świetnie! Wydaje mi się, że po prostu wprowadzę modyfikacje, których potrzebowałem, do różnych klas implementujących klasę abstrakcyjną, a następnie ponownie usunę tymczasową klasę średnią, a jeśli kiedykolwiek będę musiał wprowadzić więcej modyfikacji później, mogę to dodać. To obejście rzeczywiście zadziałało. Dzięki!
neminem
1
Twoje rozwiązanie działa świetnie. Po prostu nie mogę uwierzyć, że Visual Studio wymaga, abyś przeskoczył takie obręcze, aby zrobić coś tak powszechnego.
RB Davidson,
1
Ale jeśli użyję klasy middleClass, która nie jest klasą abstrakcyjną, to ten, kto dziedziczy klasę middleClass, nie musi już implementować metody abstrakcyjnej, to niweczy cel, jakim jest używanie klasy abstrakcyjnej w pierwszej kolejności ... Jak rozwiązać ten problem?
Darius
1
@ ti034 Nie mogę znaleźć żadnego obejścia. Więc po prostu sprawiam, że rzekomo abstrakcyjne funkcje z klasy middleClass mają pewne wartości domyślne, które mogą mi łatwo przypomnieć o ich zastąpieniu, bez konieczności zgłaszania przez kompilator błędu. Na przykład, jeśli rzekomo abstrakcyjną metodą jest zwrócenie tytułu strony, sprawię, że zwróci ciąg „Proszę zmienić tytuł”.
Darius
74

@smelch, jest lepsze rozwiązanie, bez konieczności tworzenia środkowej kontrolki, nawet do debugowania.

Czego chcemy

Najpierw zdefiniujmy ostatnią klasę i podstawową klasę abstrakcyjną.

public class MyControl : AbstractControl
...
public abstract class AbstractControl : UserControl // Also works for Form
...

Teraz potrzebujemy tylko dostawcy opisu .

public class AbstractControlDescriptionProvider<TAbstract, TBase> : TypeDescriptionProvider
{
    public AbstractControlDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(TAbstract)))
    {
    }

    public override Type GetReflectionType(Type objectType, object instance)
    {
        if (objectType == typeof(TAbstract))
            return typeof(TBase);

        return base.GetReflectionType(objectType, instance);
    }

    public override object CreateInstance(IServiceProvider provider, Type objectType, Type[] argTypes, object[] args)
    {
        if (objectType == typeof(TAbstract))
            objectType = typeof(TBase);

        return base.CreateInstance(provider, objectType, argTypes, args);
    }
}

Na koniec po prostu stosujemy atrybut TypeDescriptionProvider do kontrolki Abastract.

[TypeDescriptionProvider(typeof(AbstractControlDescriptionProvider<AbstractControl, UserControl>))]
public abstract class AbstractControl : UserControl
...

I to wszystko. Nie jest wymagana środkowa kontrola.

Klasa dostawcy może być zastosowana do dowolnej liczby baz abstrakcyjnych w tym samym rozwiązaniu.

* EDYCJA * W pliku app.config potrzebne są również następujące elementy

<appSettings>
    <add key="EnableOptimizedDesignerReloading" value="false" />
</appSettings>

Dzięki @ user3057544 za sugestię.


jucardi
źródło
1
To również zadziałało dla mnie, że używam CF 3.5, nie maTypeDescriptionProvider
Adrian Botor
4
Nie udało się to uruchomić w VS 2010, chociaż Smelch działa. Czy ktoś wie, dlaczego?
RobC
5
@RobC Designer z jakiegoś powodu jest trochę zrzędliwy. Zauważyłem, że po wdrożeniu tej poprawki musiałem wyczyścić rozwiązanie, zamknąć i ponownie uruchomić VS2010 i odbudować; wtedy pozwoliłoby mi zaprojektować podklasę.
Oblivious Sage
3
Warto zauważyć, że ponieważ ta poprawka zastępuje instancję klasy bazowej dla klasy abstrakcyjnej, elementy wizualne dodane w Projektancie dla klasy abstrakcyjnej nie będą dostępne podczas projektowania podklas.
Oblivious Sage
1
To zadziałało dla mnie, ale najpierw musiałem ponownie uruchomić VS 2013 po zbudowaniu projektu. @ObliviousSage - Dzięki za ostrzeżenie; w moim obecnym przypadku przynajmniej nie jest to problem, ale nadal warto na niego uważać.
InteXX,
10

@Smelch, dziękuję za pomocną odpowiedź, ponieważ ostatnio miałem ten sam problem.

Poniżej znajduje się niewielka zmiana w Twoim poście, aby zapobiec ostrzeżeniom dotyczącym kompilacji (poprzez umieszczenie klasy bazowej w #if DEBUGdyrektywie preprocesora):

public class Form1
#if DEBUG  
 : MiddleClass 
#else  
 : BaseForm 
#endif 
Dave Clemmer
źródło
5

Miałem podobny problem, ale znalazłem sposób na zreformowanie rzeczy, aby użyć interfejsu zamiast abstrakcyjnej klasy bazowej:

interface Base {....}

public class MyUserControl<T> : UserControl, Base
     where T : /constraint/
{ ... }

Może to nie mieć zastosowania w każdej sytuacji, ale jeśli to możliwe, skutkuje czystszym rozwiązaniem niż kompilacja warunkowa.

Jan Hettich
źródło
1
Czy możesz podać trochę bardziej kompletny przykład kodu? Staram się lepiej zrozumieć Twój projekt i również przetłumaczyć go na VB. Dzięki.
InteXX,
Wiem, że to jest stare, ale okazało się, że to najmniej hakerskie rozwiązanie. Ponieważ nadal chciałem, aby mój interfejs był powiązany z UserControl, dodałem UserControlwłaściwość do interfejsu i odwoływałem się do niej, gdy potrzebowałem bezpośredniego dostępu do niego. W moich implementacjach interfejsu rozszerzam UserControl i ustawiam UserControlwłaściwość nathis
chanban
3

Korzystam z rozwiązania w tej odpowiedzi na inne pytanie, które łączy ten artykuł . Artykuł zaleca użycie niestandardowej TypeDescriptionProvideri konkretnej implementacji klasy abstrakcyjnej. Projektant zapyta niestandardowego dostawcę, których typów użyć, a kod może zwrócić konkretną klasę, aby projektant był zadowolony, gdy masz pełną kontrolę nad wyglądem klasy abstrakcyjnej jako konkretnej klasy.

Aktualizacja: dołączyłem udokumentowany przykład kodu do mojej odpowiedzi na to drugie pytanie. Kod tam działa, ale czasami muszę przejść przez cykl czyszczenia / kompilacji, jak wskazałem w mojej odpowiedzi, aby go uruchomić.

Carl G
źródło
3

Mam kilka wskazówek dla osób, które twierdzą, że TypeDescriptionProviderJuan Carlos Diaz nie działa i nie podoba im się również kompilacja warunkowa:

Przede wszystkim może być konieczne ponowne uruchomienie programu Visual Studio, aby zmiany w kodzie działały w projektancie formularzy (musiałem, prosta przebudowa nie działała - lub nie za każdym razem).

Przedstawię moje rozwiązanie tego problemu dla przypadku abstrakcyjnej formy bazowej. Powiedzmy, że masz BaseFormklasę i chcesz, aby wszelkie oparte na niej formularze były możliwe do zaprojektowania (tak będzie Form1). To, TypeDescriptionProviderco zaprezentował Juan Carlos Diaz, również mi nie wyszło. Oto jak to działało, łącząc go z rozwiązaniem MiddleClass (przez smelch), ale bez#if DEBUG kompilacji warunkowej iz pewnymi poprawkami:

[TypeDescriptionProvider(typeof(AbstractControlDescriptionProvider<BaseForm, BaseFormMiddle2>))]   // BaseFormMiddle2 explained below
public abstract class BaseForm : Form
{
    public BaseForm()
    {
        InitializeComponent();
    }

    public abstract void SomeAbstractMethod();
}


public class Form1 : BaseForm   // Form1 is the form to be designed. As you see it's clean and you do NOTHING special here (only the the normal abstract method(s) implementation!). The developer of such form(s) doesn't have to know anything about the abstract base form problem. He just writes his form as usual.
{
    public Form1()
    {
        InitializeComponent();
    }

    public override void SomeAbstractMethod()
    {
        // implementation of BaseForm's abstract method
    }
}

Zwróć uwagę na atrybut w klasie BaseForm. Wtedy po prostu trzeba zadeklarować TypeDescriptionProvideri dwie klasy średniej , ale nie martw się, są one niewidoczne i nieistotne dla dewelopera Form1 . Pierwsza implementuje abstrakcyjne elementy członkowskie (i sprawia, że ​​klasa bazowa nie jest abstrakcyjna). Drugi jest pusty - jest po prostu wymagany do działania projektanta formularzy VS. Następnie należy przypisać drugą klasę średnią do TypeDescriptionProviderz BaseForm. Brak kompilacji warunkowej.

Miałem jeszcze dwa problemy:

  • Problem 1: Po zmianie Form1 w projektancie (lub w jakimś kodzie) ponownie dawał błąd (podczas próby ponownego otwarcia go w projektancie).
  • Problem 2: Kontrolki BaseForm zostały umieszczone nieprawidłowo, gdy rozmiar Form1 został zmieniony w projektancie, a formularz został zamknięty i ponownie otwarty w projektancie formularzy.

Pierwszy problem (możesz go nie mieć, ponieważ jest to coś, co prześladuje mnie w moim projekcie w kilku innych miejscach i zwykle powoduje wyjątek „Nie można przekonwertować typu X na typ X”). Rozwiązałem to w programie TypeDescriptionProvider, porównując nazwy typów (FullName) zamiast porównywać typy (patrz poniżej).

Drugi problem. Naprawdę nie wiem, dlaczego kontrolki formularza podstawowego nie są projektowane w klasie Form1 i ich pozycje są tracone po zmianie rozmiaru, ale obejrzałem to (nie jest to fajne rozwiązanie - jeśli wiesz coś lepszego, napisz). Po prostu ręcznie przenoszę przyciski BaseForm (które powinny znajdować się w prawym dolnym rogu) na ich właściwe pozycje w metodzie wywoływanej asynchronicznie ze zdarzenia Load w BaseForm: BeginInvoke(new Action(CorrectLayout));Moja klasa bazowa ma tylko przyciski „OK” i „Anuluj”, więc sprawa jest prosta.

class BaseFormMiddle1 : BaseForm
{
    protected BaseFormMiddle1()
    {
    }

    public override void SomeAbstractMethod()
    {
        throw new NotImplementedException();  // this method will never be called in design mode anyway
    }
}


class BaseFormMiddle2 : BaseFormMiddle1  // empty class, just to make the VS designer working
{
}

A oto nieco zmodyfikowana wersja TypeDescriptionProvider:

public class AbstractControlDescriptionProvider<TAbstract, TBase> : TypeDescriptionProvider
{
    public AbstractControlDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(TAbstract)))
    {
    }

    public override Type GetReflectionType(Type objectType, object instance)
    {
        if (objectType.FullName == typeof(TAbstract).FullName)  // corrected condition here (original condition was incorrectly giving false in my case sometimes)
            return typeof(TBase);

        return base.GetReflectionType(objectType, instance);
    }

    public override object CreateInstance(IServiceProvider provider, Type objectType, Type[] argTypes, object[] args)
    {
        if (objectType.FullName == typeof(TAbstract).FullName)  // corrected condition here (original condition was incorrectly giving false in my case sometimes)
            objectType = typeof(TBase);

        return base.CreateInstance(provider, objectType, argTypes, args);
    }
}

I to wszystko!

Nie musisz niczego wyjaśniać przyszłym twórcom formularzy opartych na Twoim BaseForm, a oni nie muszą wykonywać żadnych sztuczek, aby zaprojektować swoje formularze! Myślę, że to najczystsze rozwiązanie, jakie może być (poza repozycjonowaniem elementów sterujących).

Jeszcze jedna wskazówka:

Jeśli z jakiegoś powodu projektant nadal odmawia pracy dla Ciebie, zawsze można zrobić prosty trik zmieniając public class Form1 : BaseFormsię public class Form1 : BaseFormMiddle1(lub BaseFormMiddle2) w pliku kodu, edytując go w projektancie formularza VS, a następnie zmieniając go z powrotem. Wolę tę sztuczkę niż kompilację warunkową, ponieważ jest mniej prawdopodobne, że zapomnę i wydam niewłaściwą wersję .

PW
źródło
1
To rozwiązało problem, który miałem z rozwiązaniem Juana w VS 2013; po ponownym uruchomieniu VS sterowanie ładuje się teraz konsekwentnie.
Luke Merrett
3

Mam wskazówkę dotyczącą rozwiązania Juan Carlos Diaz. U mnie działa świetnie, ale był z tym jakiś problem. Kiedy uruchamiam VS i wchodzę w projektanta, wszystko działa dobrze. Ale po uruchomieniu rozwiązania, następnie zatrzymaj i wyjdź z niego, a następnie spróbuj wejść do projektanta, wyjątek pojawia się wielokrotnie, aż do ponownego uruchomienia VS. Ale znalazłem na to rozwiązanie - wszystko, co musisz zrobić, to dodać poniżej do pliku app.config

  <appSettings>
   <add key="EnableOptimizedDesignerReloading" value="false" />
  </appSettings>
user3057544
źródło
2

Ponieważ klasa abstrakcyjna public abstract class BaseForm: Formdaje błąd i unika korzystania z projektanta, przyszedłem z wykorzystaniem wirtualnych członków. Zasadniczo zamiast deklarować metody abstrakcyjne, zadeklarowałem metody wirtualne z możliwie najmniejszą ilością treści. Oto, co zrobiłem:

public class DataForm : Form {
    protected virtual void displayFields() {}
}

public partial class Form1 : DataForm {
    protected override void displayFields() { /* Do the stuff needed for Form1. */ }
    ...
}

public partial class Form2 : DataForm {
    protected override void displayFields() { /* Do the stuff needed for Form2. */ }
    ...
}

/* Do this for all classes that inherit from DataForm. */

Ponieważ DataFormmiała to być klasa abstrakcyjna z elementem abstrakcyjnym displayFields, „udaje” to zachowanie z członkami wirtualnymi, aby uniknąć abstrakcji. Projektant nie narzeka już i u mnie wszystko działa dobrze.

Jest to bardziej czytelne obejście, ale ponieważ nie jest abstrakcyjne, muszę się upewnić, że wszystkie klasy potomne DataFormmają implementację displayFields. Dlatego zachowaj ostrożność podczas korzystania z tej techniki.

Gabriel L.
źródło
To jest to, z czym poszedłem. Właśnie wrzuciłem NotImplementedException do klasy bazowej, aby błąd był oczywisty, jeśli został zapomniany.
Shaun Rowan
1

Projektant formularzy systemu Windows tworzy wystąpienie klasy bazowej formularza / kontrolki i stosuje wynik analizy InitializeComponent. Dlatego możesz zaprojektować formularz utworzony przez kreatora projektu, nawet bez budowania projektu. Z powodu tego zachowania nie można również zaprojektować formantu pochodzącego z klasy abstrakcyjnej.

Możesz zaimplementować te abstrakcyjne metody i zgłosić wyjątek, gdy nie jest on uruchomiony w projektancie. Programista, który wywodzi się z formantu, musi zapewnić implementację, która nie wywołuje implementacji klasy bazowej. W przeciwnym razie program ulegnie awarii.

Sheng Jiang 蒋 晟
źródło
szkoda, ale tak to się jeszcze robi. Mam nadzieję, że uda mi się to zrobić we właściwy sposób.
Oliver Friedrich
Jest lepszy sposób, zobacz odpowiedź Smelcha
Allen Rice,
-1

Możesz po prostu warunkowo wkompilować abstractsłowo kluczowe bez wstawiania oddzielnej klasy:

#if DEBUG
  // Visual Studio 2008 designer complains when a form inherits from an 
  // abstract base class
  public class BaseForm: Form {
#else
  // For production do it the *RIGHT* way.
  public abstract class BaseForm: Form {
#endif

    // Body of BaseForm goes here
  }

Działa to pod warunkiem, że BaseFormnie ma żadnych metod abstrakcyjnych ( abstractsłowo kluczowe zapobiega zatem tylko tworzeniu wystąpienia klasy w czasie wykonywania).

Peter Gluck
źródło