IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

ASP.NET MVC Discussion :

MEF : Enrichir une vue par composition/héritage


Sujet :

ASP.NET MVC

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre éclairé
    Profil pro
    Inscrit en
    Juin 2006
    Messages
    351
    Détails du profil
    Informations personnelles :
    Âge : 38
    Localisation : France, Côte d'Or (Bourgogne)

    Informations forums :
    Inscription : Juin 2006
    Messages : 351
    Par défaut MEF : Enrichir une vue par composition/héritage
    Bonjour,

    J'ai une application web ASP MVC 4 et on souhaiterais la rendre modulable via d'autres projets ASP MVC4. Concrètement il s'agit d'avoir un noyau commun pour tous nos clients et de pouvoir ajouter quelques spécificités pour certains d'entre eux par le biais de modules pluggable (un par client).

    La finalité est qu'au sein d'écrans existants, on puisse ajouter du contenu dans la page.

    Pour ce faire, j'utilise MEF pour faire de la composition de Controllers.

    Je me retrouve donc avec deux Controllers de la façon suivante :

    - Main Application
    -- Controllers
    --- TestController

    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    public virtual ActionResult Index()
    {
        return View();
    }

    - Module
    -- Controllers
    --- TestController : hérite de MainApplication.Controllers.TestController
    Code C# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
    public override ActionResult Index()
    {
        // Je veux continuer à afficher la page de base
        ViewResult baseActionResult = (ViewResult)base.Index();
        baseActionResult.ExecuteResult(this.ControllerContext);
     
        // Tout en ajoutant du nouveau contenu
        var test = new TagBuilder("h1");
        test.SetInnerText("Nouveau contenu propre au module");
        return Content(test.ToString());
    }

    Lorsque mon module est présent, je veux pouvoir continuer d'afficher la page originale (donc l'Index de ma MainApplication) tout en lui ajoutant un contenu bidon (ici mon TagBuilder).
    Evidemment, le code que j'ai mis fonctionne en partie mais le nouveau contenu n'est pas vraiment "au sein de ma page" mais "en dessous".

    Comment feriez-vous pour implémenter ce type de fonctionnalité ?

    Merci et désolé si ma demande n'est pas forcément très claire...

    Laurent

  2. #2
    Membre averti
    Inscrit en
    Octobre 2005
    Messages
    26
    Détails du profil
    Informations forums :
    Inscription : Octobre 2005
    Messages : 26
    Par défaut
    Bonjour,

    J'ai eu a faire un peu la même chose en mvc 3 avec MEF il y a un bon moment.

    la solution que j'avais trouvé était de définir un contrat (via une interface IPlugIn) à mes plugins.

    Une fois ces contrats définis, j'ai fait une classe de base à mes controller avec une propriétée IEnumerable<IPlugIn> PlugIns

    Avec Mef je faisais du DI au moment de la création du controller pour remplir la listes des plugins à la volée.

    Dans les pages ayant besoin des infos des plugins, je retournais un ViewBag.PlugIns = this.PlugIns.

    L'affichage étant prévenu qu'il pouvait avoir des infos supplémentaires via les PlugIns, j'avais tout loisir de rajouter ou modifier la view.

    Si aucun plugin n'était présent, la page de base était de toute manière affichée.

    Je n'ai malheureusement plus le code de ce projet, n'étant plus dans la société où je l'ai conçu.

    Par contre, je sais que j'avais eu de grosses difficulté sur les mises à jour des PlugIns car il faut recharger les dlls "à la volée" dans le répertoire bin... mais c'est plus un pb d'architecture d'Asp.Net qu'autre chose.

  3. #3
    Membre éclairé
    Profil pro
    Inscrit en
    Juin 2006
    Messages
    351
    Détails du profil
    Informations personnelles :
    Âge : 38
    Localisation : France, Côte d'Or (Bourgogne)

    Informations forums :
    Inscription : Juin 2006
    Messages : 351
    Par défaut
    Bonjour et merci de ta contribution,

    Pour ma part j'ai déjà trouvé une solution il y a quelques temps mais j'ai oublié de la poster

    Voici assez succinctement ma solution

    Au sein de mon module je définit:
    • Un ModuleViewEngine (correspondant à un RazorViewEngine)
    • Un ou des controllers spécifiques qui seront des nouveaux controllers ou des Controllers héritant d'un Controller de l'application commune
    • Une ou des views qui seront soit des nouvelles vues, soit des parties de vues qui seront mergées avec une vue existante de l'application commune


    Au sein de mon application principale
    • Un ModuleViewEngine abstract qui permettra aux modules éventuellement de redéfinir les chemins d'accès aux vues du module
    • Un MEFControllerFactory héritant de DefaultControllerFactory qui va rechercher via MEF les éventuels Controller qui sont (re)définis dans le module. Si MEF ne trouve pas de module ou ne trouve pas de controller du type recherché dans le module, il va utiliser le controller de l'application de base. Sinon on utilisera le controller trouvé par composition.
    • Une classe CompositionConfig permettant de récupérer par composition le ModuleViewEngine redéfinit par le module mais aussi de redéfinir le ControllerFactory à utiliser par MVC. La méthode qui fait ces actions est appelée dans le Global.asax au même endroit que sont appelées les méthodes de config des Routes.
    • Dans les controllers existants, marquer comme virtual les méthodes que l'on souhaiterait "overrider" dans le controller du module qui hériterait du controller de l'application de base. Ceci est voulu dans le cas où je souhaite étendre une vue existante en ajoutant une section de code cshtml.
    • Dans les Views existantes que l'on souhaite pouvoir étendre, il suffit de mettre un marqueur que l'on a définit comme convention. Ce marqueur sera utilisé par un Helper pour être remplacé par le contenu de l'extension de la vue écrite dans le module


    Voici les bouts de code qui vont bien

    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    public static class CompositionConfig
    {
     
    	private static CompositionContainer container;
    	public static CompositionContainer Container
    	{
    		get
    		{
    			return container;
    		}
    	}
     
    	public static void Compose(HttpApplication httpApplication)
    	{
    		// Configuration de nos catalogues d'imports
    		// Pour la composition, on ne fouille que dans les assemblys dont le nom commence par la même première partie que l'assembly courante (évite de récupérer toutes les assemblys systèmes etc.)
    		string currentAssemblyNameFirstPart = Assembly.GetAssembly(typeof(CompositionConfig)).GetName().FullName.Split('.').First();
    		string assemblyNamesFilter = string.Format("{0}.*.dll", currentAssemblyNameFirstPart);
     
    		var catalog = new AggregateCatalog();
    		catalog.Catalogs.Add(new DirectoryCatalog(httpApplication.Server.MapPath("~/bin"), assemblyNamesFilter));
     
    		// Composition
    		container = new CompositionContainer(catalog);
     
    		// Utilisation de notre Custom ControllerFactory
    		IControllerFactory factory = new MEFControllerFactory(httpApplication, container);
    		ControllerBuilder.Current.SetControllerFactory(factory);
     
    		// Utilisation du ModuleViewEngine en tant que ViewEngine de l'application MVC
    		var composedViewEngine = container.GetExports(typeof(ModuleViewEngine), null, null).SingleOrDefault();
    		if (composedViewEngine != null)
    		{
    			ViewEngines.Engines.Clear();
    			ViewEngines.Engines.Add((ModuleViewEngine)composedViewEngine.Value);
    		}
    	}
     
    }

    Dans le Global.asax, appel de la méthode de composition
    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class MvcApplication : System.Web.HttpApplication
    {
    	protected void Application_Start()
    	{
    		[...]
    		CompositionConfig.Compose(httpApplication: this);
    		[...]
    	}
    }

    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    /// <summary>
    /// Classe à hériter par chaque module.
    /// Permet notamment de définir l'emplacement des vues du module.
    /// </summary>
    public abstract class ModuleViewEngine : RazorViewEngine
    {
     
    	public ModuleViewEngine()
    	{
    		var moduleViewsLocations = this.GetModuleViewsLocations();
     
    		// Fusionne avec l'existant
    		ViewLocationFormats = moduleViewsLocations.Union(ViewLocationFormats).ToArray();
    		PartialViewLocationFormats = moduleViewsLocations.Union(PartialViewLocationFormats).ToArray();
    	}
     
    	/// <summary>
    	/// Définit l'emplacement des vues du module
    	/// Emplacement par défaut : ~/Views/Modules/{1}/{0}.cshtml
    	/// </summary>
    	public virtual string[] GetModuleViewsLocations()
    	{
    		return new[] { ModuleHelper.ModulesViewsPath + "/{1}/{0}.cshtml" };
    	}
     
    }

    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    public class MEFControllerFactory : DefaultControllerFactory
    {
     
    	private readonly CompositionContainer _container;
     
    	public MEFControllerFactory(HttpApplication callingHttpApplication, CompositionContainer container)
    	{
    		_container = container;
    		_container.ComposeParts(callingHttpApplication);
    	}
     
    	protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
    	{
    		if (controllerType == null)
    			throw new ArgumentNullException("controllerType", "controllerType is null. Expected route was " + requestContext.RouteData.ToStringRoute());
     
    		Lazy<object, object> importedController = _container.GetExports(controllerType, null, null).FirstOrDefault();
     
    		return importedController == null
    			? base.GetControllerInstance(requestContext, controllerType) // Utilisation du controller de l'application commune
    			: (IController)importedController.Value; // Utilisation du Controller obtenu par composition
    	}
     
    	public override void ReleaseController(IController controller)
    	{
    		((IDisposable)controller).Dispose();
    	}
     
    }

    Côté Controller, dans l'application principale, rien de bien original.

    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class MonController : ControllerBase
    {
    	/// <summary>
    	/// Handles the main entry point of the page related to the controller
    	/// </summary>
    	/// <returns>The Index page</returns>
    	public virtual ActionResult Index()
    	{
    		return View(this.ExecuteGetIndex(vm => vm.Initialize()));
    	}
     
    }

    Exemple d'une vue que l'on souhaite étendre dans le module. On a choisit de placer un tag <modulePart></modulePart> qui ne sert à rien d'autre que de définir l'endroit dans lequel sera placé le contenu à ajouter par le module. Cette balise peut être remplacé par n'importe quoi. Le principe est que l'on récupère notre vue sous format d'une chaine de caractère (le code source de la page web html finalement) et que l'on remplace notre fameuse balise par le code source de la vue définit par le module (via la méthode Replace de la classe String tout simplement)
    On a choisit de définir également d'autres conventions : les vues au sein des modules qui constituent des extensions de vues existantes sont suffixés par "Part" donnant par exemple le fichier IndexPart.cshtml tandis que la vue Index de l'application principale reste Index.cshtml

    Code cshtml : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
     
    @model MonModel
    @{
        ViewBag.Title = "Index";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
     
    @section js{
        <script src="~/Scripts/Views/monFichierJs.js?v=@jsFilesVersion"></script>
    }
     
    <table>
    <tr>
    <td>Contenu de ma page d'application principale</td>
    </tr>
    </table>
     
    <!-- Le contenu additionnel de mon module se situera ici -->
    <ModulePart></ModulePart>
     
    @Html.BindJavascript()

    Exemple d'une extension de vue du module

    Code cshtml : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    @{
        ViewBag.Title = "IndexPart";
        Layout = null;
    }
     
    @Scripts.Render("~/Scripts/Modules/Views/Mon/module-specificities.js")
     
    <table><tr><td>Cool l'extension !</td></tr></table>

    Pour merger les vues de l'application principale avec les vues des modules, j'ai dû écrire quelques helpers que voici. Ils permettent entre autre de récupérer le code source d'une vue cshtml compilée et de pouvoir alors remplacer la balise <ModulePart></ModulePart> par le contenu additionnel défini dans le module.

    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    public static class ComposeViewHelper
    {
     
    	// Convention : Pour ajouter du contenu provenant d'une vue d'un module, la vue parent doit contenir un Tag <ModulePart></ModulePart> dans son HTML.
    	//              Ce tag sera alors remplacé au rendu par le contenu de la vue (du module) que l'on souhaite intégré à la vue commune (projet web commun) 
    	public static readonly string ModuleHtmlTag = "<ModulePart></ModulePart>";
     
    	// Convention de nommage : Une vue enfant utilisée pour la composition doit avoir son nom suffixé par la chaine "Part" ==> ViewNamePart
    	public static readonly string ModuleViewSuffix = "Part";
     
     
    	public static ActionResult MergeViews(ControllerContext controllerContext, string parentViewName, string childViewName)
    	{
    		var parentViewSourceCode = GetViewToString(controllerContext, parentViewName);
    		var childViewSourceCode = GetViewToString(controllerContext, childViewName, parentViewName);
    		return MergeViews(parentViewSourceCode, childViewSourceCode);
    	}
     
    	public static ActionResult MergeViews(ControllerContext controllerContext, ViewResultBase parentViewResult, ViewResultBase childViewResult)
    	{
     
    		var parentViewSourceCode = GetViewToString(controllerContext, parentViewResult, isChildView: false);
    		var childViewSourceCode = GetViewToString(controllerContext, childViewResult, isChildView: true);
    		return MergeViews(parentViewSourceCode, childViewSourceCode);
    	}
     
    	public static ActionResult MergeViews(ControllerContext controllerContext, ViewResultBase parentViewResult, string childViewName)
    	{
    		var parentViewSourceCode = GetViewToString(controllerContext, parentViewResult, isChildView: false);
    		var childViewSourceCode = GetViewToString(controllerContext, childViewName, parentViewName: parentViewResult.ViewName);
    		return MergeViews(parentViewSourceCode, childViewSourceCode);
    	}
     
    	public static string GetViewToString(ControllerContext controllerContext, ViewResultBase viewResult, bool isChildView)
    	{
    		if (controllerContext == null)
    			throw new ArgumentNullException("controllerContext");
     
    		if (viewResult == null)
    			throw new ArgumentNullException("viewResult");
     
    		using (StringWriter viewStringWriter = new StringWriter())
    		{
    			viewResult.View = GetView(controllerContext, viewResult, isChildView);
    			var viewContext = new ViewContext(controllerContext, viewResult.View, viewResult.ViewData, viewResult.TempData, viewStringWriter);
    			viewResult.View.Render(viewContext, viewStringWriter);
    			ViewEngines.Engines.Single().ReleaseView(controllerContext, viewResult.View);
     
    			return viewStringWriter.GetStringBuilder().ToString();
    		}
    	}
     
    	/// <summary>
    	/// Récupère le code source HTML d'une vue MVC
    	/// </summary>
    	/// <param name="controllerContext"></param>
    	/// <param name="viewName"></param>
    	/// <param name="parentViewName">
    	/// Nom de la vue parent permettant de récupérer son emplacement et déterminer où la vue enfant doit se situer.
    	/// Exemple : Si le nom de la vue parent est : DisplayTemplates/VueParent.cshtml, la vue enfant sera DisplayTemplates/VueEnfant.cshtml
    	/// </param>
    	/// <returns>code source HTML sous forme de chaine de caractères de la vue spécifiée en paramètre</returns>
    	public static string GetViewToString(ControllerContext controllerContext, string viewName, string parentViewName = "")
    	{
    		if (controllerContext == null)
    			throw new ArgumentNullException("controllerContext");
     
    		if (string.IsNullOrEmpty(viewName))
    			throw new ArgumentNullException("viewName");
     
    		using (StringWriter viewStringWriter = new StringWriter())
    		{
    			if (!string.IsNullOrEmpty(parentViewName))
    				viewName = Path.Combine(parentViewName.Replace(Path.GetFileName(parentViewName), ""), viewName);
     
    			var viewEngineResult = ViewEngines.Engines.FindView(controllerContext, viewName, masterName: "");
    			var viewContext = new ViewContext(controllerContext, viewEngineResult.View, controllerContext.Controller.ViewData, controllerContext.Controller.TempData, viewStringWriter);
    			viewEngineResult.View.Render(viewContext, viewStringWriter);
    			viewEngineResult.ViewEngine.ReleaseView(controllerContext, viewEngineResult.View);
     
    			return viewStringWriter.GetStringBuilder().ToString();
    		}
    	}
     
    	public static ActionResult MergeViews(string parentViewSourceCode, string childViewSourceCode)
    	{
    		return new ContentResult { Content = parentViewSourceCode.Replace(ModuleHtmlTag, childViewSourceCode) };
    	}
     
    	private static IView GetView(ControllerContext controllerContext, ViewResultBase viewResult, bool isChildView)
    	{
    		if (viewResult.View != null)
    			return viewResult.View;
     
    		if (string.IsNullOrEmpty(viewResult.ViewName))
    			viewResult.ViewName = controllerContext.RouteData.GetRequiredString("action");
     
    		if (isChildView)
    			viewResult.ViewName += ModuleViewSuffix;
     
    		if (viewResult.GetType().IsGenericType)
    		{
    			if (viewResult.GetType().GetGenericTypeDefinition() == typeof(DisplayViewResult<>))
    				viewResult.ViewName = (viewResult as TemplatedViewResult).BuildViewPath(DataBoundControlMode.ReadOnly, viewResult.ViewName);
    			else if (viewResult.GetType().GetGenericTypeDefinition() == typeof(EditorViewResult<>))
    				viewResult.ViewName = (viewResult as TemplatedViewResult).BuildViewPath(DataBoundControlMode.Edit, viewResult.ViewName);
    		}
     
    		ViewEngineResult viewEngineResult = ViewEngines.Engines.FindView(controllerContext, viewResult.ViewName, masterName: "");
    		return viewEngineResult.View;
    	}
     
    }


    Maintenant côté module, on redéfinit le ModuleViewEngine même si on ne modifie pas nécessairement le comportement par défaut

    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    [Export(typeof(ModuleViewEngine))] // Permet à MEF de voir la classe StudyModuleViewEngine depuis l'application principale
    public class StudyModuleViewEngine : ModuleViewEngine
    {
     
    	// Pas d'override de méthode, on conserve le comportement par défaut du ModuleViewEngine défini dans l'application principale
     
    }

    Côté Controller, il faut simplement ceci

    Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    [Export(typeof(MonController))]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class ModuleMonController : MonController
    {
     
    	public override ActionResult Index()
    	{
    		var baseViewResult = base.Index(); // Exécution du code du parent sans modification
     
    		var composedViewActionResult = ComposeViewHelper.MergeViews(this.ControllerContext, (ViewResultBase)baseViewResult, "IndexPart"); // On merge la vue générée par la méthode parent avec le contenu additionnel écrit dans notre module
     
    		return composedViewActionResult;
    	}
     
    }

    Je pense ne rien avoir oublié.

    Il faut voir que cette solution est adapté à notre besoin, d'autres solutions existent peut être

    N'hésitez pas en cas de questions ou de bouts manquants

  4. #4
    Membre averti
    Inscrit en
    Octobre 2005
    Messages
    26
    Détails du profil
    Informations forums :
    Inscription : Octobre 2005
    Messages : 26
    Par défaut
    Très intéressant, même si je ne suis pas très fan des remplacement dans les chaines de caractères (trop eu de "surprises" dues aux saisies utilisateurs).

    J'ai une question concernant l'ajout d'un plugin (donc d'une dll), d'après ce que je comprend le chargement se fait uniquement au démarrage de l'application puisque vous mettez les dll dans le dossier bin, vous perdez donc toutes les variables de sessions et autre valeurs en mémoire (sauf si vous les persistez quelque part ?).

    C'est la problématique la plus grande que j'ai eu sur mon projet car les plugins devaient être intégrés dynamiquement dans le site (via un formulaire) et ne pas avoir a relancer l'application (donc pas de chargement au niveau du global.asax) ni perdre les sessions en cours.

    Sinon beau travail !

  5. #5
    Membre éclairé
    Profil pro
    Inscrit en
    Juin 2006
    Messages
    351
    Détails du profil
    Informations personnelles :
    Âge : 38
    Localisation : France, Côte d'Or (Bourgogne)

    Informations forums :
    Inscription : Juin 2006
    Messages : 351
    Par défaut
    La saisie utilisateur ne dérange pas puisque c'est au chargement de la vue que l'on fait le remplacement. Et la chaine de remplacement n'est pas commune non plus.

    De notre côté on est en stateless, les paramètres sont conservés ou envoyés par le biais de l'URL. Pas d'histoire de perte de session du coup et quand bien même, la session n'interfère pas avec la phase de composition et d'instanciation des controllers.
    De notre côté on ne fait la phase de composition qu'au démarrage, mais ma méthode CompositionConfig.Compose() peut être appelée à n'importe quel moment dans mon code. On peut très bien recomposer à chaque changement de page par exemple, ou au clic sur un bouton, ce qui rechargerait mes modules.

    Merci pour le compliment, j'avoue que ce fut long avant de trouver la solution adaptée à MVC car les embuches sont grandes

  6. #6
    Membre averti
    Inscrit en
    Octobre 2005
    Messages
    26
    Détails du profil
    Informations forums :
    Inscription : Octobre 2005
    Messages : 26
    Par défaut
    Citation Envoyé par marcusien Voir le message
    La saisie utilisateur ne dérange pas puisque c'est au chargement de la vue que l'on fait le remplacement.
    Je voulais dire les données inscrites en base de données par les utilisateurs et qui doivent être affichées dans la vue en cours (ou la vue composée). Mais c'est un autre problème hors du spectre de la question initiale.

Discussions similaires

  1. [9.3] Alimentation d'une vue par la dernière valeur d'une donnée
    Par NDevoucoux dans le forum Requêtes
    Réponses: 2
    Dernier message: 02/10/2014, 10h56
  2. [HF17] Alimenter une vue par Procédure Stockée ou Trigger
    Par fbe66 dans le forum HyperFileSQL
    Réponses: 0
    Dernier message: 24/10/2013, 01h32
  3. Modification d'éléments d'une vue par une servlet
    Par quentin.pigne dans le forum Servlets/JSP
    Réponses: 2
    Dernier message: 31/05/2013, 18h12
  4. Tri d'une vue par n° de commande
    Par juju05 dans le forum Langage SQL
    Réponses: 16
    Dernier message: 21/06/2011, 18h23
  5. Importer un fichier csv dans une vue par DTS
    Par lemordore dans le forum MS SQL Server
    Réponses: 2
    Dernier message: 11/02/2008, 12h39

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo